/**
* Copyright 2016 LinkedIn Corp. 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.
*/
package com.github.ambry.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class to measure and throttle the rate of some process. The throttler takes a desired rate-per-second
* (the units of the process do not matter, it could be bytes or a count of some other thing), and will sleep for
* an appropriate amount of time when maybeThrottle() is called to attain the desired rate.
*/
public class Throttler {
private double desiredRatePerSec;
private long checkIntervalMs;
private boolean throttleDown;
private Object lock = new Object();
private Object waitGuard = new Object();
private long periodStartNs;
private double observedSoFar;
private Logger logger = LoggerFactory.getLogger(getClass());
private Time time;
private boolean enabled;
/**
* @param desiredRatePerSec: The rate we want to hit in units/sec
* @param checkIntervalMs: The interval at which to check our rate
* @param throttleDown: Does throttling increase or decrease our rate?
* @param time: The time implementation to use
**/
public Throttler(double desiredRatePerSec, long checkIntervalMs, boolean throttleDown, Time time) {
this.desiredRatePerSec = desiredRatePerSec;
this.checkIntervalMs = checkIntervalMs;
this.throttleDown = throttleDown;
this.time = time;
this.observedSoFar = 0.0;
this.periodStartNs = time.nanoseconds();
this.enabled = true;
}
/**
* Throttle if required
* @param observed the newly observed units since the last time this method was called.
*/
public void maybeThrottle(double observed) throws InterruptedException {
synchronized (lock) {
observedSoFar += observed;
long now = time.nanoseconds();
long elapsedNs = now - periodStartNs;
// if we have completed an interval AND we have observed something, maybe
// we should take a little nap
if (elapsedNs > checkIntervalMs * Time.NsPerMs && observedSoFar > 0) {
double rateInSecs = (observedSoFar * Time.NsPerSec) / elapsedNs;
boolean needAdjustment = !(throttleDown ^ (rateInSecs > desiredRatePerSec));
if (needAdjustment) {
// solve for the amount of time to sleep to make us hit the desired rate
double desiredRateMs = desiredRatePerSec / Time.MsPerSec;
double elapsedMs = elapsedNs / Time.NsPerMs;
long sleepTime = Math.round(observedSoFar / desiredRateMs - elapsedMs);
if (sleepTime > 0) {
logger.trace("Natural rate is {} per second but desired rate is {}, sleeping for {} ms to compensate.",
rateInSecs, desiredRatePerSec, sleepTime);
synchronized (waitGuard) {
if (enabled) {
time.wait(waitGuard, sleepTime);
}
}
}
}
periodStartNs = now;
observedSoFar = 0;
}
}
}
/**
* Disable the throttler for good.
*/
public void close() {
synchronized (waitGuard) {
enabled = false;
waitGuard.notify();
}
}
}