/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.client.metrics;

import alluxio.ClientContext;
import alluxio.Constants;
import alluxio.exception.status.AlluxioStatusException;
import alluxio.exception.status.UnavailableException;
import alluxio.grpc.ClientMetrics;
import alluxio.master.MasterClientContext;
import alluxio.master.MasterInquireClient;
import alluxio.metrics.MetricsSystem;
import alluxio.util.logging.SamplingLogger;

import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Task that carries the client metrics information to master through heartheat. This class manages
 * its own {@link MetricsMasterClient}.
 *
 * If the task fails to heartbeat to the master, it will destroy its old master client and recreate
 * it before retrying.
 */
@ThreadSafe
public final class ClientMasterSync {
  private static final Logger SAMPLING_LOG =
      new SamplingLogger(LoggerFactory.getLogger(ClientMasterSync.class),
          30L * Constants.SECOND_MS);

  private final String mApplicationId;
  private final MasterInquireClient mInquireClient;
  private final ClientContext mContext;

  /**
   * Client for communicating to metrics master.
   */
  private RetryHandlingMetricsMasterClient mMasterClient;

  /**
   * Constructs a new {@link ClientMasterSync}.
   *
   * @param appId the application id to send with metrics
   * @param ctx client context
   * @param inquireClient the master inquire client
   */
  public ClientMasterSync(String appId, ClientContext ctx, MasterInquireClient inquireClient) {
    mApplicationId = Preconditions.checkNotNull(appId);
    mInquireClient = inquireClient;
    mContext = ctx;
  }

  /**
   * Sends metrics to the master keyed with appId and client hostname.
   */
  public synchronized void heartbeat() {
    if (mMasterClient == null) {
      if (loadConf()) {
        mMasterClient = new RetryHandlingMetricsMasterClient(MasterClientContext
            .newBuilder(mContext)
            .setMasterInquireClient(mInquireClient)
            .build());
      } else {
        return; // not heartbeat when failed to load conf
      }
    }
    // TODO(zac): Support per FileSystem instance metrics
    // Currently we only support JVM-level metrics. A list is used here because in the near
    // future we will support sending per filesystem client-level metrics.
    List<alluxio.grpc.ClientMetrics> fsClientMetrics = new ArrayList<>();
    List<alluxio.grpc.Metric> metrics = MetricsSystem.reportClientMetrics();
    if (metrics.size() == 0) {
      // Likely when all should report metrics are counters
      // and this client doesn't do any actual operations (only used for meta sync)
      return;
    }
    fsClientMetrics.add(ClientMetrics.newBuilder()
        .setSource(mApplicationId)
        .addAllMetrics(metrics)
        .build());
    try {
      mMasterClient.heartbeat(fsClientMetrics);
    } catch (IOException e) {
      // WARN instead of ERROR as metrics are not critical to the application function
      SAMPLING_LOG.warn("Failed to send metrics to master: {}", e.toString());
    }
  }

  /**
   * Close the metrics master client.
   */
  public synchronized void close() {
    if (mMasterClient != null) {
      mMasterClient.close();
    }
  }

  /**
   * Loads configuration.
   *
   * @return true if successfully loaded configuration
   */
  private boolean loadConf() {
    try {
      InetSocketAddress masterAddr = mInquireClient.getPrimaryRpcAddress();
      mContext.loadConf(masterAddr);
    } catch (UnavailableException e) {
      SAMPLING_LOG.error("Failed to get master address during initialization", e);
      return false;
    } catch (AlluxioStatusException ae) {
      SAMPLING_LOG.error("Failed to load configuration from meta master during initialization", ae);
      return false;
    }
    return true;
  }
}
