org.voltdb.client.exampleutils.RateLimiter Maven / Gradle / Ivy
/* This file is part of VoltDB.
* Copyright (C) 2008-2020 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see .
*/
package org.voltdb.client.exampleutils;
import java.util.ArrayList;
/**
* A simple rate limiter that can be used to bind an asynchronous application/benchmark to a maximum transactional rate, preventing firehosing or providing a fixed expected throughput for performance validation (latency review at specific rates).
*
* @author Seb Coursol
* @since 2.0
*/
public class RateLimiter implements IRateLimiter
{
private long SleepTime;
private int StepCount;
private Long[] CycleStepDuration;
private Long[] CycleStepMaxCount;
private Long[] CycleStepCount;
private Long[] CycleStepEndTime;
private long MaxProcessPerSecond = 0l;
private long CycleAdjustment = Long.MIN_VALUE;
private long LastCycleCount = Long.MAX_VALUE;
/**
* Creates a new rate limiter.
*
* @param maxProcessPerSecond the maximum number of requests the limiter should allow per second.
*/
public RateLimiter(long maxProcessPerSecond)
{
this.set(maxProcessPerSecond, 0l, false);
}
/**
* {@inheritDoc}
*/
public void throttle()
{
this.throttle(this.MaxProcessPerSecond);
}
/**
* Throttle the execution process and re-adjust the rate requirement on the fly. This call allows the calling application to dynamically adjust the rate over time, for instance as a result of some external changes or analysis. For an example of such an application, see {@link LatencyLimiter}.
*
* @param maxProcessPerSecond the maximum number of requests the limiter should allow per second.
*/
public void throttle(long maxProcessPerSecond)
{
long adjustment = 0l;
boolean forceSet = false;
if (this.CycleAdjustment <= System.currentTimeMillis())
{
if ((this.MaxProcessPerSecond == maxProcessPerSecond) && (this.LastCycleCount != Long.MAX_VALUE))
{
if (this.MaxProcessPerSecond > this.LastCycleCount)
{
adjustment = this.MaxProcessPerSecond - this.LastCycleCount;
}
else
forceSet = true;
}
this.CycleAdjustment = System.currentTimeMillis() + 1000l;
this.LastCycleCount = 0l;
}
this.set(maxProcessPerSecond, adjustment, forceSet);
try
{
// For rates below 1/ms a pre-empting sleep is the best way to approach the desired rate without
// impacting latency (see note in loop below)
if (this.SleepTime > 0)
Thread.sleep(this.SleepTime);
for(int i=0;i= this.CycleStepMaxCount[i])
{
// If the Cycle duration is more than 1 ms, we might have to wait for a considerable time.
// - sleeping is more CPU-efficient than spinning, so we'll sleep just a bit less so we're
// considerate without blowing our window.
// Because of Java's threading model, and the fact the callbacks are executed in the calling
// thread, low execution rates will experience artificial latency (to the order of a few
// milliseconds).
long sleepDuration = this.CycleStepEndTime[i]-System.currentTimeMillis()-1;
if (sleepDuration > 0)
Thread.sleep(sleepDuration);
// 1ms or less to wait - Spin until the time changes
while (System.currentTimeMillis() < this.CycleStepEndTime[i]);
// Reset counters
this.CycleStepEndTime[i] = System.currentTimeMillis() + this.CycleStepDuration[i];
this.CycleStepCount[i] = 0l;
}
}
this.LastCycleCount++;
}
catch(Exception tie) {}
}
/**
* Adjusts the rate limit, whether through automatic adjustment based on monitoring of executed versus requested rates, or as a consequence of an external request to modify the expected rate.
*
* @param maxProcessPerSecond the requested maximum number of requests per second.
* @param adjustment the adjustment calculated internally by the limiter by comparing actual executions versus the requested rate.
* @param forceSet the flag indicating this call is a conseuqnce to an external rate-change request and the distribution of executions over time should be recalculated.
*/
private void set(long maxProcessPerSecond, long adjustment, boolean forceSet)
{
if ((this.MaxProcessPerSecond != maxProcessPerSecond) || (adjustment != 0l) || forceSet)
{
this.MaxProcessPerSecond = maxProcessPerSecond;
maxProcessPerSecond += adjustment;
// Ruth's tests fail with a divide by zero (ENG-2426).
// So the quick fix is to set maxProcessPerSecond to 1000 if it is rate limited to zero by the latency limiter.
// Basically set a floor.
if (0 == maxProcessPerSecond)
maxProcessPerSecond=1000;
// For rates below 1/ms we can sleep a while between each execution, which is the most efficient approach.
this.SleepTime = (1000l/maxProcessPerSecond)-1l;
// Calculate the largest cycle duration based on requested rate
long gcd = MathEx.gcd(maxProcessPerSecond,1000l);
// Calculate incremental sub-cycles that will be used to approach the desired rate (1s, 100ms, 10ms, 1ms (or a
// similar variation if the largest cycle duration is less than 1s))
double cycleDuration = (1000l/gcd);
double cycleMaxCount = (maxProcessPerSecond/gcd);
ArrayList cycleStepDuration = new ArrayList();
ArrayList cycleStepMaxCount = new ArrayList();
ArrayList cycleStepCount = new ArrayList();
while((cycleDuration >= 1d) && (cycleMaxCount > 0d))
{
cycleStepDuration.add((long)cycleDuration);
cycleStepMaxCount.add((long)cycleMaxCount);
cycleStepCount.add(0l);
cycleDuration = cycleDuration/10d;
cycleMaxCount = Math.ceil(cycleMaxCount/10d);
}
// Convert ArrayLists to arrays (easier/faster to work with)
this.StepCount = cycleStepCount.size();
this.CycleStepDuration = cycleStepDuration.toArray(new Long[this.StepCount]);
this.CycleStepMaxCount = cycleStepMaxCount.toArray(new Long[this.StepCount]);
this.CycleStepCount = cycleStepCount.toArray(new Long[this.StepCount]);
this.CycleStepEndTime = cycleStepCount.toArray(new Long[this.StepCount]);
// Initialize cycle end times
for(int i=0;i