package com.bizo.asperatus.rates; import static com.google.common.base.Preconditions.checkNotNull; import static java.util.concurrent.TimeUnit.SECONDS; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import com.google.common.base.Stopwatch; /** * This class is a basic thread-safe implementation of RateTracker. * * The recommended use pattern is to use a scheduled executor service to periodically call "getRateAndResetPeriod" and * pass the result to a metric tracking service (ie, Asperatus/CloudWatch). */ public final class ThreadSafeRateTracker implements RateTracker { /* * This read/write lock is used to control flushes to the underlying metric tracker. The "read" trackLock should be * acquired prior to updating the counter. The "write" flushLock should be acquired prior to resetting the counter and * stopwatch. */ private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock trackLock = lock.readLock(); private final Lock flushLock = lock.writeLock(); /** Number of events since last flush. Acquire trackLock before incrementing and flushLock before resetting. */ private final AtomicLong counter = new AtomicLong(0L); /** Measures the length of the current period. Acquire flushLock before modifying. */ private final Stopwatch stopwatch; /** Constructor that uses the system time to track the duration of each reporting period. */ public ThreadSafeRateTracker() { this(Stopwatch.createUnstarted()); } /** Constructor that allows passing a specific Stopwatch implementation. Intended for testing. */ public ThreadSafeRateTracker(final Stopwatch stopwatch) { checkNotNull(stopwatch); this.stopwatch = stopwatch; this.stopwatch.start(); } @Override public void track() { trackLock.lock(); try { counter.incrementAndGet(); } finally { trackLock.unlock(); } } @Override public double getRateAndResetPeriod() { final double count; final double periodLengthSeconds; flushLock.lock(); try { stopwatch.stop(); periodLengthSeconds = stopwatch.elapsed(SECONDS); count = counter.getAndSet(0L); stopwatch.reset(); stopwatch.start(); } finally { flushLock.unlock(); } return count / periodLengthSeconds; } }