All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.api.control.Client Maven / Gradle / Ivy

There is a newer version: 1.0.14
Show newest version
/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed 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 com.google.api.control;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.control.aggregator.CheckAggregationOptions;
import com.google.api.control.aggregator.CheckRequestAggregator;
import com.google.api.control.aggregator.QuotaAggregationOptions;
import com.google.api.control.aggregator.QuotaRequestAggregator;
import com.google.api.control.aggregator.ReportAggregationOptions;
import com.google.api.control.aggregator.ReportRequestAggregator;
import com.google.api.control.model.KnownLabels;
import com.google.api.servicecontrol.v1.AllocateQuotaRequest;
import com.google.api.servicecontrol.v1.AllocateQuotaResponse;
import com.google.api.servicecontrol.v1.CheckRequest;
import com.google.api.servicecontrol.v1.CheckResponse;
import com.google.api.servicecontrol.v1.ReportRequest;
import com.google.api.services.servicecontrol.v1.ServiceControl;
import com.google.api.services.servicecontrol.v1.ServiceControlScopes;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
import com.google.common.collect.Queues;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.PriorityQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;

/**
 * Client is a package-level facade that encapsulates all service control functionality.
 */
public class Client {
  private static final Logger log = Logger.getLogger(Client.class.getName());
  private static final String CLIENT_APPLICATION_NAME = "Service Control Client";
  private static final String BACKGROUND_THREAD_ERROR =
      "The scheduler thread was unable to start. This means that metric reporting will only be "
      + "done periodically after requests are served. If your API is low-traffic (below 1 query "
      + "per second), this may result in delays in reporting.";
  private static final int MAX_IDLE_TIME_SECONDS = 120;
  public static final int DO_NOT_LOG_STATS = -1;
  public static final SchedulerFactory DEFAULT_SCHEDULER_FACTORY = new SchedulerFactory() {
    @Override
    public Scheduler create(Ticker ticker) {
      return new Scheduler(ticker);
    }
  };
  private final CheckRequestAggregator checkAggregator;
  private final ReportRequestAggregator reportAggregator;
  private final QuotaRequestAggregator quotaAggregator;
  private final Ticker ticker;
  private final ThreadFactory threads;
  private final SchedulerFactory schedulers;
  private final ServiceControl transport;
  private boolean running;
  private boolean stopped;
  private Scheduler scheduler;
  private String serviceName;
  private Statistics statistics;
  private Thread schedulerThread;
  private int statsLogFrequency;
  private final Stopwatch reportStopwatch;

  public Client(String serviceName, CheckAggregationOptions checkOptions,
      ReportAggregationOptions reportOptions, QuotaAggregationOptions quotaOptions,
      ServiceControl transport, ThreadFactory threads,
      SchedulerFactory schedulers, int statsLogFrequency, @Nullable Ticker ticker) {
    ticker = ticker == null ? Ticker.systemTicker() : ticker;
    this.checkAggregator = new CheckRequestAggregator(serviceName, checkOptions, null, ticker);
    this.reportAggregator = new ReportRequestAggregator(serviceName, reportOptions, null, ticker);
    this.quotaAggregator = new QuotaRequestAggregator(serviceName, quotaOptions, ticker);
    this.serviceName = serviceName;
    this.ticker = ticker;
    this.transport = transport;
    this.threads = threads;
    this.schedulers = schedulers;
    this.scheduler  = null; // the scheduler is assigned when start is invoked
    this.schedulerThread = null;
    this.statsLogFrequency = statsLogFrequency;
    this.statistics = new Statistics();
    this.reportStopwatch = Stopwatch.createUnstarted(ticker);
  }

  /**
   * Starts processing.
   *
   * Calling this method
   *
   * starts the thread that regularly flushes all enabled caches. enables the other methods on the
   * instance to be called successfully
   *
   * I.e, even when the configuration disables aggregation, it is invalid to access the other
   * methods of an instance until ``start`` is called - Calls to other public methods will fail with
   * an {@code IllegalStateError}.
   */
  public synchronized void start() {
    if (running) {
      log.log(Level.INFO, String.format("%s is already started", this));
      return;
    }
    log.log(Level.INFO, String.format("starting %s", this));
    this.stopped = false;
    this.running = true;
    this.reportStopwatch.reset().start();
    try {
      schedulerThread = threads.newThread(new Runnable() {
        @Override
        public void run() {
          scheduleFlushes();
        }
      });
      schedulerThread.start();
    } catch (RuntimeException e) {
      log.log(Level.INFO, BACKGROUND_THREAD_ERROR);
      schedulerThread = null;
      initializeFlushing();
    }
  }

  /** Starts processing if it hasn't already started. */
  public synchronized void startIfStopped() {
    if (!running) {
      start();
    }
  }

  /**
   * Stops processing.
   *
   * Does not block waiting for processing to stop, but after this called, the scheduler thread if
   * it's active will come to a close
   */
  public void stop() {
    Preconditions.checkState(running, "Cannot stop if it's not running");

    synchronized (this) {
      log.log(Level.INFO, "stopping client background thread and flushing the report aggregator");
      for (ReportRequest req : reportAggregator.clear()) {
        try {
          transport.services().report(serviceName, req).execute();
        } catch (IOException e) {
          log.log(Level.SEVERE,
              String.format("direct send of a report request failed because of %s", e));
        }
      }
      this.stopped = true;  // the scheduler thread will set running to false
      if (isRunningSchedulerDirectly()) {
        resetIfStopped();
      }
      this.scheduler = null;
    }
  }

  /**
   * Process a check request.
   *
   * The {@code req} is first passed to the {@code CheckAggregator}. If there is a valid cached
   * response, that is returned, otherwise a response is obtained from the transport.
   *
   * @param req a {@link CheckRequest}
   * @return a {@link CheckResponse} or {@code null} if none was cached and there was a transport
   *         failure
   */
  public @Nullable CheckResponse check(CheckRequest req) {
    startIfStopped();
    statistics.totalChecks.incrementAndGet();
    Stopwatch w = Stopwatch.createStarted(ticker);
    CheckResponse resp = checkAggregator.check(req);
    statistics.totalCheckCacheLookupTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
    if (resp != null) {
      statistics.checkHits.incrementAndGet();
      if (log.isLoggable(Level.FINER)) {
        log.log(Level.FINER, String.format("using cached check response for %s: %s", req, resp));
      }
      return resp;
    }

    // Application code should not fail (or be blocked) because check request's do not succeed.
    // Instead they should fail open so here just simply log the error and return None to indicate
    // that no response was obtained.
    try {
      w.reset().start();
      resp = transport.services().check(serviceName, req).execute();
      statistics.totalCheckTransportTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
      checkAggregator.addResponse(req, resp);
      return resp;
    } catch (IOException e) {
      log.log(Level.SEVERE,
          String.format("direct send of a check request %s failed because of %s", req, e));
      return null;
    }
  }

  public AllocateQuotaResponse allocateQuota(AllocateQuotaRequest req) {
    startIfStopped();
    statistics.totalQuotas.incrementAndGet();
    Stopwatch w = Stopwatch.createStarted(ticker);
    AllocateQuotaResponse resp = quotaAggregator.allocateQuota(req);
    statistics.totalQuotaCacheLookupTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
    if (resp != null) {
      statistics.quotaHits.incrementAndGet();
      return resp;
    }
    try {
      w.reset().start();
      resp = transport.services().allocateQuota(serviceName, req).execute();
      statistics.totalQuotaTransportTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
      quotaAggregator.cacheResponse(req, resp);
      return resp;
    } catch (IOException e) {
      log.log(Level.SEVERE,
          String.format("direct send of a quota request %s failed because of %s", req, e));
      AllocateQuotaResponse dummyResponse = AllocateQuotaResponse.getDefaultInstance();
      quotaAggregator.cacheResponse(req, dummyResponse);
      return dummyResponse;
    }
  }

  /**
   * Process a report request.
   *
   * The {@code req} is first passed to the {@code ReportAggregator}. It will either be aggregated
   * with prior requests or sent immediately
   *
   * @param req a {@link ReportRequest}
   */
  public void report(ReportRequest req) {
    startIfStopped();
    statistics.totalReports.incrementAndGet();
    statistics.reportedOperations.addAndGet(req.getOperationsCount());
    Stopwatch w = Stopwatch.createStarted(ticker);
    boolean reported = reportAggregator.report(req);
    statistics.totalReportCacheUpdateTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
    if (!reported) {
      try {
        statistics.directReports.incrementAndGet();
        w.reset().start();
        transport.services().report(serviceName, req).execute();
        statistics.totalTransportedReportTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
      } catch (IOException e) {
        log.log(Level.SEVERE,
            String.format("direct send of a report request %s failed because of %s", req, e));
      }
    }

    if (isRunningSchedulerDirectly()) {
      try {
        scheduler.run(false /* don't block */);
      } catch (InterruptedException e) {
        log.log(Level.SEVERE,
            String.format("direct run of scheduler failed because of %s", e));
      }
    }
    logStatistics();
  }

  private void logStatistics() {
    if (statsLogFrequency < 1) {
      return;
    }
    if (statistics.totalReports.get() % statsLogFrequency == 0) {
      log.info(statistics.toString());
    }
  }

  private void scheduleFlushes() {
    try {
      initializeFlushing();
      this.scheduler.run(); // if caching is configured, this blocks until stop is called
      log.log(Level.INFO, String.format("scheduler %s has no further tasks and will exit", this));
      this.scheduler = null;
    } catch (InterruptedException e) {
      log.log(Level.SEVERE, String.format("scheduler %s was interrupted and exited", this), e);
      this.stopped = true;
    } catch (RuntimeException e) {
      log.log(Level.SEVERE, String.format("scheduler %s failed and exited", this), e);
      this.stopped = true;
    }
  }

  private boolean isRunningSchedulerDirectly() {
    return running && schedulerThread == null;
  }

  private synchronized void initializeFlushing() {
    log.info("creating a scheduler to control flushing");
    this.scheduler = schedulers.create(ticker);
    this.scheduler.setStatistics(statistics);
    log.info("scheduling the initial check, report, and quota");
    flushAndScheduleChecks();
    flushAndScheduleReports();
    flushAndScheduleQuota();
  }

  private synchronized boolean resetIfStopped() {
    if (!stopped) {
      return false;
    }

    // It's stopped to let's cleanup
    checkAggregator.clear();
    reportAggregator.clear();
    quotaAggregator.clear();
    running = false;
    return true;
  }

  private void flushAndScheduleChecks() {
    if (resetIfStopped()) {
      log.log(Level.FINE, "did not schedule check flush: client is stopped");
      return;
    }
    int interval = checkAggregator.getFlushIntervalMillis();
    if (interval < 0) {
      log.log(Level.FINE, "did not schedule check flush: caching is disabled");
      return; // cache is disabled, so no flushing it
    }

    if (isRunningSchedulerDirectly()) {
      log.log(Level.FINE, "did not schedule check flush: no scheduler thread is running");
      return;
    }

    log.log(Level.FINE, "flushing the check aggregator");
    Stopwatch w = Stopwatch.createUnstarted(ticker);
    for (CheckRequest req : checkAggregator.flush()) {
      try {
        statistics.recachedChecks.incrementAndGet();
        w.reset().start();
        CheckResponse resp = transport.services().check(serviceName, req).execute();
        statistics.totalCheckTransportTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
        w.reset().start();
        checkAggregator.addResponse(req, resp);
        statistics.totalCheckCacheUpdateTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
      } catch (IOException e) {
        log.log(Level.SEVERE,
            String.format("direct send of a check request %s failed because of %s", req, e));
      }
    }
    scheduler.enter(new Runnable() {
      @Override
      public void run() {
        flushAndScheduleChecks(); // Do this again after the interval
      }
    }, interval, 0 /* high priority */);
  }

  private void flushAndScheduleReports() {
    if (resetIfStopped()) {
      log.log(Level.FINE, "did not schedule report flush: client is stopped");
      return;
    }
    int interval = reportAggregator.getFlushIntervalMillis();
    if (interval < 0) {
      log.log(Level.FINE, "did not schedule report flush: cache is disabled");
      return; // cache is disabled, so no flushing it
    }
    ReportRequest[] flushed = reportAggregator.flush();
    if (log.isLoggable(Level.FINE)) {
      log.log(Level.FINE,
          String.format("flushing %d reports from the report aggregator", flushed.length));
    }
    statistics.flushedReports.addAndGet(flushed.length);
    Stopwatch w = Stopwatch.createUnstarted(ticker);
    for (ReportRequest req : flushed) {
      try {
        statistics.flushedOperations.addAndGet(req.getOperationsCount());
        w.reset().start();
        transport.services().report(serviceName, req).execute();
        statistics.totalTransportedReportTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
      } catch (IOException e) {
        log.log(Level.SEVERE,
            String.format("direct send of a report request failed because of %s", e));
      }
    }
    if (flushed.length > 0) {
      reportStopwatch.reset().start();
    } else if (reportStopwatch.elapsed(TimeUnit.SECONDS) > MAX_IDLE_TIME_SECONDS) {
      log.log(Level.INFO,
          String.format(
              "Shutting down after no reports in the last %d seconds.", MAX_IDLE_TIME_SECONDS));
      stop();
      return;
    }
    scheduler.enter(new Runnable() {
      @Override
      public void run() {
        flushAndScheduleReports(); // Do this again after the interval
      }
    }, interval, 1 /* not so high priority */);
  }

  private void flushAndScheduleQuota() {
    if (resetIfStopped()) {
      log.log(Level.FINE, "did not schedule quota flush: client is stopped");
      return;
    }
    int interval = quotaAggregator.getFlushIntervalMillis();
    if (interval < 0) {
      log.log(Level.FINE, "did not schedule quota flush: caching is disabled");
      return; // cache is disabled, so no flushing it
    }

    if (isRunningSchedulerDirectly()) {
      log.log(Level.FINE, "did not schedule check flush: no scheduler thread is running");
      return;
    }

    log.log(Level.FINE, "flushing the quota aggregator");
    Stopwatch w = Stopwatch.createUnstarted(ticker);
    List reqs = quotaAggregator.flush();
    if (log.isLoggable(Level.FINE)) {
      log.log(Level.FINE,
          String.format("flushing %d quota from the quota aggregator", reqs.size()));
    }
    for (AllocateQuotaRequest req : reqs) {
      try {
        w.reset().start();
        AllocateQuotaResponse resp = transport.services().allocateQuota(serviceName, req).execute();
        statistics.totalQuotaTransportTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
        w.reset().start();
        quotaAggregator.cacheResponse(req, resp);
        statistics.recachedQuotas.incrementAndGet();
        statistics.totalQuotaCacheUpdateTimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
      } catch (IOException e) {
        log.log(Level.SEVERE,
            String.format("direct send of a quota request %s failed because of %s", req, e));
      }
    }
    scheduler.enter(new Runnable() {
      @Override
      public void run() {
        flushAndScheduleQuota(); // Do this again after the interval
      }
    }, interval, 0 /* high priority */);
  }

  /**
   * Builder provide structure to the construction of a {@link Client}
   */
  public static class Builder {
    private int statsLogFrequency;
    private Ticker ticker;
    private HttpTransport transport;
    private ThreadFactory factory;
    private String serviceName;
    private CheckAggregationOptions checkOptions;
    private ReportAggregationOptions reportOptions;
    private QuotaAggregationOptions quotaOptions;
    private SchedulerFactory schedulerFactory = DEFAULT_SCHEDULER_FACTORY;

    public Builder(String name) {
      this.serviceName = name;
    }

    public Builder setTicker(Ticker ticker) {
      this.ticker = ticker;
      return this;
    }

    public Builder setStatsLogFrequency(int frequency) {
      this.statsLogFrequency = frequency;
      return this;
    }

    public Builder setCheckOptions(CheckAggregationOptions options) {
      this.checkOptions = options;
      return this;
    }

    public Builder setReportOptions(ReportAggregationOptions options) {
      this.reportOptions = options;
      return this;
    }

    public Builder setQuotaOptions(QuotaAggregationOptions options) {
      this.quotaOptions = options;
      return this;
    }

    public Builder setHttpTransport(HttpTransport transport) {
      this.transport = transport;
      return this;
    }

    public Builder setFactory(ThreadFactory factory) {
      this.factory = factory;
      return this;
    }

    public Builder setSchedulerFactory(SchedulerFactory f) {
      this.schedulerFactory = f;
      return this;
    }

    public Client build() throws GeneralSecurityException, IOException {
      HttpTransport h = this.transport;
      if (h == null) {
        h = GoogleNetHttpTransport.newTrustedTransport();
      }
      GoogleCredential c = GoogleCredential.getApplicationDefault(transport, new JacksonFactory());
      if (c.createScopedRequired()) {
        c = c.createScoped(ServiceControlScopes.all());
      }
      ThreadFactory f = this.factory;
      if (f == null) {
        f = new ThreadFactoryBuilder().build();
      }
      CheckAggregationOptions o = this.checkOptions;
      if (o == null) {
        o = new CheckAggregationOptions();
      }
      ReportAggregationOptions r = this.reportOptions;
      if (r == null) {
        r = new ReportAggregationOptions();
      }
      QuotaAggregationOptions q = this.quotaOptions;
      if (q == null) {
        q = new QuotaAggregationOptions();
      }
      final GoogleCredential nestedInitializer = c;
      HttpRequestInitializer addUserAgent = new HttpRequestInitializer() {
        @Override
        public void initialize(HttpRequest request) throws IOException {
          HttpHeaders hdr = new HttpHeaders().setUserAgent(KnownLabels.USER_AGENT);
          request.setHeaders(hdr);
          nestedInitializer.initialize(request);
        }
      };
      return new Client(serviceName, o, r, q,
          new ServiceControl.Builder(h, c)
              .setHttpRequestInitializer(addUserAgent)
              .setApplicationName(CLIENT_APPLICATION_NAME)
              .build(),
          f, schedulerFactory, statsLogFrequency, ticker);
    }
  }

  /**
   * Statistics contains information about the performance of a {@code Client}.
   */
  static class Statistics {
    // counts
    AtomicLong checkHits = new AtomicLong();
    AtomicLong quotaHits = new AtomicLong();

    AtomicLong directReports = new AtomicLong();
    AtomicLong flushedOperations = new AtomicLong();
    AtomicLong flushedReports = new AtomicLong();
    AtomicLong recachedChecks = new AtomicLong();
    AtomicLong recachedQuotas = new AtomicLong();
    AtomicLong reportedOperations = new AtomicLong();
    AtomicLong totalChecks = new AtomicLong();
    AtomicLong totalReports = new AtomicLong();
    AtomicLong totalQuotas = new AtomicLong();
    AtomicLong totalSchedulerRuns = new AtomicLong();
    AtomicLong totalSchedulerSkips = new AtomicLong();

    // latencies
    AtomicLong totalCheckCacheLookupTimeMillis = new AtomicLong();
    AtomicLong totalCheckCacheUpdateTimeMillis = new AtomicLong();
    AtomicLong totalCheckTransportTimeMillis = new AtomicLong();
    AtomicLong totalTransportedReportTimeMillis = new AtomicLong();
    AtomicLong totalReportCacheUpdateTimeMillis = new AtomicLong();
    AtomicLong totalQuotaCacheLookupTimeMillis = new AtomicLong();
    AtomicLong totalQuotaCacheUpdateTimeMillis = new AtomicLong();
    AtomicLong totalQuotaTransportTimeMillis = new AtomicLong();
    AtomicLong totalSchedulerSkiptimeMillis = new AtomicLong();
    AtomicLong totalSchedulerRuntimeMillis = new AtomicLong();

    public double checkHitsPercent() {
      return divide(100 * checkHits.get(), totalChecks.get());
    }

    public double flushedReportsPercent() {
      return divide(100 * flushedReports.get(), totalReports.get());
    }

    public long directChecks() {
      return totalChecks.get() - checkHits.get();
    }

    public long totalChecksTransported() {
      return directChecks() + recachedChecks.get();
    }

    public long totalReportsTransported() {
      return directReports.get() + flushedReports.get();
    }

    public double meanTransportedReportTimeMillis() {
      return divide(totalTransportedReportTimeMillis.get(), totalReportsTransported());
    }

    public double meanReportCacheUpdateTimeMillis() {
      long count = totalReports.get() - directReports.get();
      return divide(totalReportCacheUpdateTimeMillis.get(), count);
    }

    public double meanTransportedCheckTimeMillis() {
      return divide(totalCheckTransportTimeMillis.get(), totalChecksTransported());
    }

    public double meanCheckCacheLookupTimeMillis() {
      return divide(totalCheckCacheLookupTimeMillis, totalChecks);
    }

    public double meanCheckCacheUpdateTimeMillis() {
      return divide(totalCheckCacheUpdateTimeMillis.get(), totalChecksTransported());
    }

    private static double divide(AtomicLong dividend, AtomicLong divisor) {
      return divide(dividend.get(), divisor.get());
    }

    private static double divide(long dividend, long divisor) {
      if (divisor == 0) {
        return 0;
      }
      return 1.0 * dividend / divisor;
    }

    @Override
    public String toString() {
      final String nl = "\n  "; // Use a consistent space to make the output valid YAML
      return "statistics:"
          + nl + "totalChecks:" + totalChecks.get()
          + nl + "checkHits:" + checkHits.get()
          + nl + "checkHitsPercent:" + checkHitsPercent()
          + nl + "recachedChecks:" + recachedChecks.get()
          + nl + "totalChecksTransported:" + totalChecksTransported()
          + nl + "totalTransportedCheckTimeMillis:" + totalCheckTransportTimeMillis.get()
          + nl + "meanTransportedCheckTimeMillis:" + meanTransportedCheckTimeMillis()
          + nl + "totalCheckCacheLookupTimeMillis:" + totalCheckCacheLookupTimeMillis.get()
          + nl + "meanCheckCacheLookupTimeMillis:" + meanCheckCacheLookupTimeMillis()
          + nl + "totalCheckCacheUpdateTimeMillis:" + totalCheckCacheUpdateTimeMillis.get()
          + nl + "meanCheckCacheUpdateTimeMillis:" + meanCheckCacheUpdateTimeMillis()
          + nl + "totalReports:" + totalReports.get()
          + nl + "flushedReports:" + flushedReports.get()
          + nl + "directReports:" + directReports.get()
          + nl + "flushedReportsPercent:" + flushedReportsPercent()
          + nl + "totalReportsTransported:" + totalReportsTransported()
          + nl + "totalTransportedReportTimeMillis:" + totalTransportedReportTimeMillis.get()
          + nl + "meanTransportedReportTimeMillis:" + meanTransportedReportTimeMillis()
          + nl + "totalReportCacheUpdateTimeMillis:" + totalReportCacheUpdateTimeMillis.get()
          + nl + "meanReportCacheUpdateTimeMillis:" + meanReportCacheUpdateTimeMillis()
          + nl + "flushedOperations:" + flushedOperations.get()
          + nl + "reportedOperations:" + reportedOperations.get()
          + nl + "totalSchedulerRuns:" + totalSchedulerRuns.get()
          + nl + "totalSchedulerRuntimeMillis:" + totalSchedulerRuntimeMillis.get()
          + nl + "meanSchedulerRuntimeMillis:" + divide(totalSchedulerRuntimeMillis, totalSchedulerRuns)
          + nl + "totalSchedulerSkips:" + totalSchedulerSkips.get()
          + nl + "totalSchedulerSkiptimeMillis:" + totalSchedulerSkiptimeMillis.get()
          + nl + "meanSchedulerSkiptimeMillis:" + divide(totalSchedulerSkiptimeMillis, totalSchedulerSkips);
    }
  }

  /**
   * SchedulerFactory defines a method for creating {@link Scheduler} instances
   */
  interface SchedulerFactory {
    /**
     * @param ticker obtains time updates from a time source.
     * @return a {@ link Scheduler}
     */
    Scheduler create(Ticker ticker);
  }

  /**
   * Scheduler uses a {@code PriorityQueue} to maintain a series of {@link Runnable}
   */
  static class Scheduler {
    private PriorityQueue queue;
    private Ticker ticker;
    private Statistics statistics;

    Scheduler(Ticker ticker) {
      this.queue = Queues.newPriorityQueue();
      this.ticker = ticker;
    }

    /**
     * @param r a {@code Runnable} to run after {@code deltaNanos}
     * @param deltaMillis the time in the future to run {@code r}
     * @param priority the priority at which to give running {@code r}
     */
    public void enter(Runnable r, long deltaMillis, int priority) {
      long later = TimeUnit.MILLISECONDS.toNanos(deltaMillis) + ticker.read();
      ScheduledEvent event = new ScheduledEvent(r, later, priority);
      synchronized (this) {
        queue.add(event);
      }
    }

    public void run(boolean block) throws InterruptedException {
      while (!this.queue.isEmpty()) {
        boolean delay = true;
        ScheduledEvent next = null;
        long gap = 0;
        synchronized (this) {
          next = queue.peek();
          long now = ticker.read();
          gap = next.getTickerTime() - now;
          if (gap > 0) {
            delay = true;
            next = null;
          } else {
            next = queue.remove();
            delay = false;
          }
        }
        if (delay) {
          long gapMillis = TimeUnit.NANOSECONDS.toMillis(gap);
          if (statistics != null) {
            statistics.totalSchedulerSkips.incrementAndGet();
            statistics.totalSchedulerSkiptimeMillis.addAndGet(gapMillis);
          }
          if (!block) {
            if (log.isLoggable(Level.FINE)) {
              log.log(Level.FINE,
                  String.format("Scheduler on %s was not blocking, next event is in %d",
                      Thread.currentThread(), gapMillis));
            }
            return;
          }
          if (log.isLoggable(Level.FINE)) {
            log.log(Level.FINE, String.format("Scheduler on %s will sleep for %d millis",
                Thread.currentThread(), gapMillis));
          }
          Thread.sleep(gapMillis);
        } else {
          if (log.isLoggable(Level.FINE)) {
            log.log(Level.FINE,
                String.format("Scheduler on %s will run an event", Thread.currentThread()));
          }
          Stopwatch w = Stopwatch.createStarted(ticker);
          next.getScheduledAction().run();
          if (statistics != null) {
            statistics.totalSchedulerRuns.incrementAndGet();
            statistics.totalSchedulerRuntimeMillis.addAndGet(w.elapsed(TimeUnit.MILLISECONDS));
          }
        }
      }
    }

    public void run() throws InterruptedException {
      run(true);
    }

    public void setStatistics(Statistics statistics) {
      this.statistics = statistics;
    }
  }

  /**
   * ScheduledEvent holds the data for scheduling an event to be run the scheduling thread.
   *
   * It is {@link Comparable}, with the compare function coded so that the low values (i.e earlier
   * times) take precedence. Lower priority is also takes precedence.
   */
  private static class ScheduledEvent implements Comparable {
    private Runnable scheduledAction;
    private long tickerTime;
    private int priority;

    public ScheduledEvent(Runnable scheduledAction, long tickerTime, int priority) {
      this.scheduledAction = scheduledAction;
      this.tickerTime = tickerTime;
      this.priority = priority;
    }

    public Runnable getScheduledAction() {
      return scheduledAction;
    }

    public long getTickerTime() {
      return tickerTime;
    }

    @Override
    public int compareTo(ScheduledEvent o) {
      int timeCompare = Long.compare(this.tickerTime, o.tickerTime);
      if (timeCompare != 0) {
        return timeCompare;
      }
      return Long.compare(this.priority, o.priority);
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + priority;
      result = prime * result + (int) (tickerTime ^ (tickerTime >>> 32));
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (getClass() != obj.getClass()) {
        return false;
      }
      ScheduledEvent other = (ScheduledEvent) obj;
      if (tickerTime != other.tickerTime) {
        return false;
      }
      if (priority != other.priority) {
        return false;
      }
      return true;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy