com.twitter.hbc.BasicRateTracker Maven / Gradle / Ivy
/**
* Copyright 2013 Twitter, Inc.
* 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.twitter.hbc;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Tracks the rate of a recurring event with a sliding window.
* Threadsafe
*/
public class BasicRateTracker implements RateTracker {
private final int granularityMillis;
private final int numBuckets;
private final RateUpdater rateUpdater;
private ScheduledFuture> future;
private final ScheduledExecutorService executor;
public BasicRateTracker(int granularityMillis, int numBuckets, boolean startPaused, ScheduledExecutorService executor) {
Preconditions.checkArgument(numBuckets > 0);
Preconditions.checkArgument(granularityMillis > 0);
Preconditions.checkArgument(granularityMillis / numBuckets > 0);
this.granularityMillis = granularityMillis;
this.numBuckets = numBuckets;
this.executor = Preconditions.checkNotNull(executor);
this.rateUpdater = new RateUpdater(startPaused);
}
@Override
public void eventObserved() {
rateUpdater.eventObserved();
}
/**
* Pauses the rate tracker: the rate will be frozen.
*/
@Override
public void pause() {
rateUpdater.pause();
}
@Override
public void resume() {
rateUpdater.resume();
}
@Override
public void start() {
this.future = executor.scheduleAtFixedRate(rateUpdater, granularityMillis / numBuckets, granularityMillis / numBuckets, TimeUnit.MILLISECONDS);
}
/**
* Stops tracking the rate
*/
@Override
public void stop() {
if (future != null) {
future.cancel(false);
}
}
/**
* Stops and shuts down the underlying executor
*/
@Override
public void shutdown() {
stop();
executor.shutdown();
}
/**
* Only used for testing
*/
@VisibleForTesting
void recalculate() {
rateUpdater.run();
}
/**
* @return the current rate if it is available, NaN if not.
* The rate is unavailable if granularityMillis
hasn't elapsed
*/
public double getCurrentRateSeconds() {
return rateUpdater.getCurrentRateSeconds();
}
class RateUpdater implements Runnable {
private final int[] buckets;
private final Object lock;
private boolean paused;
private double rate;
private boolean rateValid;
private int total;
private int currentBucket;
private boolean previouslyPaused;
private int currentBucketCount;
RateUpdater() {
this(false);
}
RateUpdater(boolean paused) {
this.rateValid = false;
this.buckets = new int[numBuckets];
this.paused = paused;
this.rate = Double.NaN;
this.lock = new Object();
}
@Override
public void run() {
synchronized (lock) {
int currentCount = currentBucketCount;
currentBucketCount = 0;
if (paused) {
previouslyPaused = true;
return;
}
// skip the first estimation after a pause, since it could be in the middle of an estimation
if (previouslyPaused) {
previouslyPaused = false;
return;
}
int prevBucket = currentBucket;
currentBucket = (currentBucket + 1) % numBuckets;
if (currentBucket == 0) {
// we've wrapped around again. this rate is now valid
rateValid = true;
}
int prevBucketCount = buckets[prevBucket];
buckets[prevBucket] = currentCount;
total += currentCount - prevBucketCount;
if (rateValid) {
rate = total * 1000d / granularityMillis;
}
}
}
public void eventObserved() {
synchronized (lock) {
currentBucketCount++;
}
}
public void pause() {
synchronized (lock) {
paused = true;
}
}
public void resume() {
synchronized (lock) {
paused = false;
}
}
public double getCurrentRateSeconds() {
synchronized (lock) {
return rate;
}
}
}
}