/**
   * Return a host report based on the current state (similar to <code>gstat -a</code>).
   *
   * <p>Note: The report will not be accurate immediately as the {@link GangliaService} needs to
   * build up a model of the current state of the monitored hosts.
   *
   * @param reportOn The metrics to be reported for each host. The {@link IHostReport#getMetrics()}
   *     is an ordered map and will reflect the metrics in the order in which they are requested
   *     here.
   * @param comparator The comparator used to order the {@link IHostReport}s.
   * @return The {@link IHostReport}s for each known host ordered by the given {@link Comparator}.
   */
  public IHostReport[] getHostReport(
      final String[] reportOn, final Comparator<IHostReport> comparator) {

    final String[] hosts = gangliaState.getKnownHosts();

    return getHostReport(hosts, reportOn, comparator);
  }
  /** Run the ganglia service. */
  public void run() {

    GangliaListener gangliaListener = null;

    try {

      final ThreadFactory threadFactory = new DaemonThreadFactory("GangliaService");

      // Timestamp when the service starts.
      if (!serviceStartTime.compareAndSet(0L, System.currentTimeMillis())) {

        throw new IllegalStateException("Already running.");
      }

      /*
       * Start thread pools.
       */

      if (listen) listenService = Executors.newSingleThreadExecutor(threadFactory);

      if (report) sendService = Executors.newSingleThreadExecutor(threadFactory);

      /*
       * corePoolSize := HeartBeatTask + GatherTask + PurgeMetricsTask
       */
      scheduledService = Executors.newScheduledThreadPool(3, threadFactory);

      // Setup sender.
      gangliaSender = new GangliaSender(metricsServers, IGangliaDefaults.BUFFER_SIZE);

      /*
       * Start processes.
       */

      if (listen) {

        // Setup listener.
        gangliaListener =
            new GangliaListener(
                listenGroup, listenPort, messageDecoder, new GangliaServiceHandler());

        // Wrap as Future.
        listenerFuture = new FutureTask<Void>(gangliaListener);

        // Start listener
        listenService.submit(listenerFuture);
      }

      if (report) {

        if (heartbeatInterval > 0) {

          /*
           * Heartbeat for the host.
           *
           * Note: When ZERO (0), we assume that gmond is running and
           * that it will take care of this metric for us.
           *
           * Note: DO NOT enable the heartbeat if gmond is running on
           * the host. The heartbeat is the start time of gmond.
           * Having two different heartbeats for the same host will
           * look like gmond is being bounced every time a heatbeat
           * is sent out by the GangliaService or gmond!
           */

          scheduledService.scheduleWithFixedDelay(
              new HeartBeatTask(), initialDelay, heartbeatInterval, TimeUnit.SECONDS);
        }

        // metric collection and reporting.
        scheduledService.scheduleAtFixedRate( //
            new GatherMetricsTask(), //
            initialDelay, //
            monitoringInterval, // period
            TimeUnit.SECONDS // units
            );

        // Schedule task to prune old hosts and metrics.
        scheduledService.scheduleAtFixedRate(
            new PurgeMetricsTask(), 60 /* initialDelay */, 60 /* period */, TimeUnit.SECONDS);
      }

      if (log.isInfoEnabled()) log.info("Running on " + hostName + " for " + serviceName);

      if (listen) {

        /*
         * Blocks while the listener is running or until this thread is
         * interrupted.
         */

        listenerFuture.get();

      } else {

        /*
         * Wait until the server is terminated.
         */

        synchronized (keepAlive) {
          try {

            keepAlive.wait();

          } catch (InterruptedException ex) {

            // Ignore.

          }
        }
      }

    } catch (InterruptedException t) {

      // Ignore. Normal shutdown.

    } catch (Throwable t) {

      log.error(t, t);

    } finally {

      if (listenerFuture != null) {
        listenerFuture.cancel(true /* mayInterruptIfRunning */);
      }

      if (listenService != null) {
        listenService.shutdownNow();
      }

      if (gangliaSender != null) {
        /*
         * Send through a mock message. The GangliaListener can be
         * blocked in DatagramSocket.receive(). This will get it
         * unblocked so it can exit in a timely manner.
         *
         * TODO Java 7 supports non-blocking multicast channels. We
         * would not have to do this with a listener class which is
         * using NIO multicast support since it would not be blocked
         * until the next packet.
         */
        sendMessage(new GangliaRequestMessage(hostName, "shutdown", false /* spoof */));
      }

      if (sendService != null) {
        sendService.shutdownNow();
      }

      if (gangliaSender != null) gangliaSender.close();

      // Reset the soft state.
      gangliaState.reset();

      // Clear the service start time.
      serviceStartTime.set(0L);
    }
  }
  /**
   * Return a host report based on the current state (similar to <code>gstat -a</code>).
   *
   * <p>Note: The report will not be accurate immediately as the {@link GangliaService} needs to
   * build up a model of the current state of the monitored hosts.
   *
   * @param hosts The hosts for which host reports will be returned.
   * @param reportOn The metrics to be reported for each host. The {@link IHostReport#getMetrics()}
   *     is an ordered map and will reflect the metrics in the order in which they are requested
   *     here.
   * @param comparator The comparator used to order the {@link IHostReport}s (optional).
   * @return The {@link IHostReport}s for each specified host ordered by the given {@link
   *     Comparator}.
   */
  public IHostReport[] getHostReport(
      final String[] hosts, final String[] reportOn, final Comparator<IHostReport> comparator) {

    if (reportOn == null || reportOn.length == 0) throw new IllegalArgumentException();

    //		if (comparator == null)
    //			throw new IllegalArgumentException();

    final IHostReport[] a = new IHostReport[hosts.length];

    for (int i = 0; i < a.length; i++) {

      final String hostName = hosts[i];

      // Note: This map preserves the insert order of the metrics.
      final Map<String, IGangliaMetricMessage> m =
          new LinkedHashMap<String, IGangliaMetricMessage>();

      for (String metricName : reportOn) {

        final TimestampMetricValue tmv = gangliaState.getMetric(hostName, metricName);

        if (tmv == null) {
          // No score for that metric for that host.
          continue;
        }

        final Object value = tmv.getValue();

        if (value == null) {
          // Should never happen.
          continue;
        }

        final IGangliaMetricMessage metricValue =
            metricFactory.newMetricMessage(hostName, tmv.getMetadata(), false /* spoof */, value);

        if (log.isDebugEnabled())
          log.debug(
              "host="
                  + hostName
                  + ", metric="
                  + metricName
                  + ", value="
                  + value
                  + ", record="
                  + metricValue);

        // Mock up a metric record.
        m.put(metricName /* metricName */, metricValue);
      }

      a[i] = new HostReport(hostName, m);
    }

    // Sort
    if (comparator != null) {

      Arrays.sort(a, comparator);
    }

    return a;
  }
  /**
   * Return the factory for metric declarations. If you supply a {@link GangliaMetadataFactory}
   * instance to the constructor (which is the default behavior for the reduced constructor) then
   * you can extend the default behavior in order to get nice metadata declarations for your
   * application metrics.
   */
  public IGangliaMetadataFactory getMetadataFactory() {

    return gangliaState.getMetadataFactory();
  }
  /**
   * {@inheritDoc}
   *
   * <p>This routine is typically invoked by {@link IGangliaMetricsCollector}s. However, you can
   * also simply invoke it directly.
   *
   * <p>Note: In order to get a nice metadata declaration for the record, the application should
   * also register an {@link IGangliaMetadataFactory}.
   *
   * @param metricName The name of the metric.
   * @param value The metric value.
   * @see #getMetadataFactory()
   */
  @Override
  public void setMetric(final String metricName, final Object value) {

    final GangliaSender sender = this.gangliaSender;

    if (sender != null) {

      try {

        if (metricName == null) {
          log.warn("Metric was emitted with no name.");
          return;
        }

        if (value == null) {
          log.warn("Metric was emitted with a null value: metricName=" + metricName);
          return;
        }

        IGangliaMetadataMessage decl;
        {

          // Look for a pre-declared metric.
          decl = gangliaState.getMetadata(metricName);

          if (decl == null) {

            // Obtain declaration.
            decl = gangliaState.getMetadataFactory().newDecl(hostName, metricName, value);

            if (decl == null) {

              log.error("Could not declare: " + metricName);
              return;
            }

            // Atomically declare/resolve.
            decl = gangliaState.putIfAbsent(decl);
          }
        }

        /*
         * Lookup the current value for the metric.
         *
         * Note: At this point we are guaranteed that a metadata
         * declaration exists so the return value must be non-null.
         */
        final TimestampMetricValue tmv =
            gangliaState.getMetric(getHostName(), decl.getMetricName());

        // Return must be non-null since declaration exists.
        assert tmv != null;

        // Should be the same declaration reference.
        assert decl == tmv.getMetadata();

        if (tmv.getTimestamp() == 0L) {

          /*
           * Send metadata record.
           *
           * TODO Since the captured metadata record might have
           * originated on another host, we should build a new
           * metadata record either now (so we send out the record
           * with our host name) or when we capture the record (as
           * part of obtaining a richer metadata record object).
           */

          sendMessage(tmv.getMetadata());
        }

        // Update the current metric value.
        if (tmv.setValue(value)) {

          /*
           * Send the metric record.
           *
           * Note: Either the metric value has never been transmitted,
           * or it has changed significantly, or TMax might expire if
           * we do not retransmit it now.
           */

          // update the timestamp when sending out the metric value.
          tmv.update();

          final IGangliaMetricMessage msg =
              metricFactory.newMetricMessage(getHostName(), decl, false /* spoof */, value);

          sendMessage(msg);
        }

      } catch (Throwable e) {

        log.warn(e, e);
      }
    }
  }