/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.hbase.metrics.impl;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.hadoop.hbase.metrics.MetricRegistries;
import org.apache.hadoop.hbase.metrics.MetricRegistry;
import org.apache.hadoop.hbase.metrics.MetricRegistryInfo;
import org.apache.hadoop.metrics2.MetricsCollector;
import org.apache.hadoop.metrics2.MetricsExecutor;
import org.apache.hadoop.metrics2.MetricsSource;
import org.apache.hadoop.metrics2.impl.JmxCacheBuster;
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystemHelper;
import org.apache.hadoop.metrics2.lib.MetricsExecutorImpl;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;

/**
 * This class acts as an adapter to export the MetricRegistry's in the global registry. Each
 * MetricRegistry will be registered or unregistered from the metric2 system. The collection will
 * be performed via the MetricsSourceAdapter and the MetricRegistry will collected like a
 * BaseSource instance for a group of metrics  (like WAL, RPC, etc) with the MetricRegistryInfo's
 * JMX context.
 *
 * <p>Developer note:
 * Unlike the current metrics2 based approach, the new metrics approach
 * (hbase-metrics-api and hbase-metrics modules) work by having different MetricRegistries that are
 * initialized and used from the code that lives in their respective modules (hbase-server, etc).
 * There is no need to define BaseSource classes and do a lot of indirection. The MetricRegistry'es
 * will be in the global MetricRegistriesImpl, and this class will iterate over
 * MetricRegistries.global() and register adapters to the metrics2 subsystem. These adapters then
 * report the actual values by delegating to
 * {@link HBaseMetrics2HadoopMetricsAdapter#snapshotAllMetrics(MetricRegistry, MetricsCollector)}.
 *
 * We do not initialize the Hadoop Metrics2 system assuming that other BaseSources already do so
 * (see BaseSourceImpl). Once the last BaseSource is moved to the new system, the metric2
 * initialization should be moved here.
 * </p>
 */
@InterfaceAudience.Private
public final class GlobalMetricRegistriesAdapter {

  private static final Logger LOG = LoggerFactory.getLogger(GlobalMetricRegistriesAdapter.class);

  private class MetricsSourceAdapter implements MetricsSource {
    private final MetricRegistry registry;
    MetricsSourceAdapter(MetricRegistry registry) {
      this.registry = registry;
    }

    @Override
    public void getMetrics(MetricsCollector collector, boolean all) {
      metricsAdapter.snapshotAllMetrics(registry, collector);
    }
  }

  private final MetricsExecutor executor;
  private final AtomicBoolean stopped;
  private final DefaultMetricsSystemHelper helper;
  private final HBaseMetrics2HadoopMetricsAdapter metricsAdapter;
  private final HashMap<MetricRegistryInfo, MetricsSourceAdapter> registeredSources;

  private GlobalMetricRegistriesAdapter() {
    this.executor = new MetricsExecutorImpl();
    this.stopped = new AtomicBoolean(false);
    this.metricsAdapter = new HBaseMetrics2HadoopMetricsAdapter();
    this.registeredSources = new HashMap<>();
    this.helper = new DefaultMetricsSystemHelper();
    executor.getExecutor().scheduleAtFixedRate(() -> this.doRun(), 10, 10, TimeUnit.SECONDS);
  }

  /**
   * Make sure that this global MetricSource for hbase-metrics module based metrics are initialized.
   * This should be called only once.
   */
  public static GlobalMetricRegistriesAdapter init() {
    return new GlobalMetricRegistriesAdapter();
  }

  @VisibleForTesting
  public void stop() {
    stopped.set(true);
  }

  private void doRun() {
    if (stopped.get()) {
      executor.stop();
      return;
    }
    if (LOG.isTraceEnabled()) {
      LOG.trace("doRun called: " + registeredSources);
    }

    Collection<MetricRegistry> registries = MetricRegistries.global().getMetricRegistries();
    for (MetricRegistry registry : registries) {
      MetricRegistryInfo info = registry.getMetricRegistryInfo();

      if (info.isExistingSource()) {
        // If there is an already existing BaseSource for this MetricRegistry, skip it here. These
        // types of registries are there only due to existing BaseSource implementations in the
        // source code (like MetricsRegionServer, etc). This is to make sure that we can transition
        // iteratively to the new hbase-metrics system. These type of MetricRegistry metrics will be
        // exported from the BaseSource.getMetrics() call directly because there is already a
        // MetricRecordBuilder there (see MetricsRegionServerSourceImpl).
        continue;
      }

      if (!registeredSources.containsKey(info)) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Registering adapter for the MetricRegistry: " + info.getMetricsJmxContext());
        }
        // register this as a MetricSource under different JMX Context'es.
        MetricsSourceAdapter adapter = new MetricsSourceAdapter(registry);
        LOG.info("Registering " + info.getMetricsJmxContext() + " " + info.getMetricsDescription());
        DefaultMetricsSystem.instance().register(info.getMetricsJmxContext(),
            info.getMetricsDescription(), adapter);
        registeredSources.put(info, adapter);
        // next collection will collect the newly registered MetricSource. Doing this here leads to
        // ConcurrentModificationException.
      }
    }

    boolean removed = false;
    // Remove registered sources if it is removed from the global registry
    for (Iterator<Entry<MetricRegistryInfo, MetricsSourceAdapter>> it =
         registeredSources.entrySet().iterator(); it.hasNext();) {
      Entry<MetricRegistryInfo, MetricsSourceAdapter> entry = it.next();
      MetricRegistryInfo info = entry.getKey();
      Optional<MetricRegistry> found = MetricRegistries.global().get(info);
      if (!found.isPresent()) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Removing adapter for the MetricRegistry: " + info.getMetricsJmxContext());
        }
        synchronized(DefaultMetricsSystem.instance()) {
          DefaultMetricsSystem.instance().unregisterSource(info.getMetricsJmxContext());
          helper.removeSourceName(info.getMetricsJmxContext());
          helper.removeObjectName(info.getMetricsJmxContext());
          it.remove();
          removed = true;
        }
      }
    }
    if (removed) {
      JmxCacheBuster.clearJmxCache();
    }
  }
}
