/* This file is part of VoltDB.
* Copyright (C) 2008-2017 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 <http://www.gnu.org/licenses/>.
*/
package org.voltdb.client;
import java.util.ArrayDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.google_voltpatches.common.base.Throwables;
/**
* Provide the {@link Client} with a way to throttle throughput in one
* of several ways. First, it can cap outstanding transactions or
* limit the rate of new transactions. Second, it can auto-tune the
* send rate to get a good balance of througput and latency on the
* server.
*
*/
class RateLimiter {
final int BLOCK_SIZE = 100; // ms
final int HISTORY_SIZE = 25;
final int RECENT_HISTORY_SIZE = 5;
final int MINIMUM_MOVEMENT = 5;
//Boolean indicating whether the only thing being tracked is max outstanding
protected boolean m_doesAnyTuning = false;
protected boolean m_autoTune = false;
protected int m_targetTxnsPerSecond = Integer.MAX_VALUE;
//protected int m_targetTxnsPerBlock = Integer.MAX_VALUE;
protected int m_latencyTarget = 5;
protected int m_currentBlockSendCount = 0;
protected int m_currentBlockRecvSuccessCount = 0;
protected int m_outstandingTxns = 0;
protected Semaphore m_outstandingTxnsSemaphore = new Semaphore(10);
protected int m_maxOutstandingTxns = 10;
protected long m_currentBlockTimestamp = -1;
protected long m_currentBlockTotalInternalLatency = 0;
protected ArrayDeque<Double> m_prevInternalLatencyAvgs = new ArrayDeque<Double>();
protected void autoTuneTargetFromHistory() {
double recentLatency = 0, mediumTermLatency = 0;
if (m_prevInternalLatencyAvgs.size() > 0) {
int i = 0;
for (double value : m_prevInternalLatencyAvgs) {
if (i < RECENT_HISTORY_SIZE) recentLatency += value;
mediumTermLatency += value;
++i;
}
recentLatency /= Math.min(m_prevInternalLatencyAvgs.size(), RECENT_HISTORY_SIZE);
mediumTermLatency /= m_prevInternalLatencyAvgs.size();
}
if ((mediumTermLatency > m_latencyTarget) && (recentLatency > m_latencyTarget)) {
m_maxOutstandingTxns -= Math.max(0.1 * m_maxOutstandingTxns, MINIMUM_MOVEMENT);
}
else if ((mediumTermLatency < m_latencyTarget) && (recentLatency > m_latencyTarget)) {
--m_maxOutstandingTxns;
}
else if ((mediumTermLatency > m_latencyTarget) && (recentLatency < m_latencyTarget)) {
m_maxOutstandingTxns++;
}
else { // if ((mediumTermLatency < m_latencyTarget) && (recentLatency < m_latencyTarget)) {
m_maxOutstandingTxns += Math.max(0.1 * m_maxOutstandingTxns, MINIMUM_MOVEMENT);
}
// don't let this go to 0, latency be damned
if (m_maxOutstandingTxns <= 0) {
m_maxOutstandingTxns = 1;
}
}
protected void ensureCurrentBlockIsKosher(long timestamp) {
long thisBlock = timestamp - (timestamp % BLOCK_SIZE);
// handle first time initialization
if (m_currentBlockTimestamp == -1) {
m_currentBlockTimestamp = thisBlock;
}
// handle time moving backwards (a bit)
if (thisBlock < m_currentBlockTimestamp) {
thisBlock = m_currentBlockTimestamp;
}
// check for new block
if (thisBlock > m_currentBlockTimestamp) {
// need to deal with 100ms skips here TODO
m_currentBlockTimestamp = thisBlock;
m_prevInternalLatencyAvgs.addFirst(
m_currentBlockTotalInternalLatency / (double) m_currentBlockRecvSuccessCount);
while (m_prevInternalLatencyAvgs.size() > HISTORY_SIZE) {
m_prevInternalLatencyAvgs.pollLast();
}
m_currentBlockSendCount = 0;
m_currentBlockRecvSuccessCount = 0;
m_currentBlockTotalInternalLatency = 0;
if (m_autoTune) {
autoTuneTargetFromHistory();
}
}
}
/**
* May not be reflected until the next 100ms.
*/
synchronized void enableAutoTuning(int latencyTarget) {
m_autoTune = true;
m_doesAnyTuning = true;
m_targetTxnsPerSecond = Integer.MAX_VALUE;
m_maxOutstandingTxns = 20;
m_latencyTarget = latencyTarget;
}
/**
* May not be reflected until the next 100ms.
*/
synchronized void setLimits(int txnsPerSec, int maxOutstanding) {
m_autoTune = false;
/*
* If the rate limit is some reasonably low value then go through the effort
* of rate limiting
*/
if (txnsPerSec < Integer.MAX_VALUE / 2) {
m_doesAnyTuning = true;
}
m_targetTxnsPerSecond = txnsPerSec;
m_maxOutstandingTxns = maxOutstanding;
m_outstandingTxnsSemaphore.drainPermits();
m_outstandingTxnsSemaphore.release(maxOutstanding);
}
/**
* Get the instantaneous values of the rate limiting values for this client.
* @return A length-2 array of integers representing max throughput/sec and
* max outstanding txns.
*/
synchronized int[] getLimits() {
int[] limits = new int[2];
limits[0] = m_targetTxnsPerSecond;
limits[1] = m_maxOutstandingTxns;
return limits;
}
/**
*
* @param timestampNanos The time as measured when the call is made.
* @param internalLatency Latency measurement of this transaction in millis
* @param ignoreBackpressure Don't return a permit for backpressure purposes since none was ever taken
*/
void transactionResponseReceived(long timestampNanos, int internalLatency, boolean ignoreBackpressure) {
if (m_doesAnyTuning) {
synchronized (this) {
ensureCurrentBlockIsKosher(TimeUnit.NANOSECONDS.toMillis(timestampNanos));
--m_outstandingTxns;
assert(m_outstandingTxns >= 0);
if (internalLatency != -1) {
++m_currentBlockRecvSuccessCount;
m_currentBlockTotalInternalLatency += internalLatency;
}
}
} else {
if (ignoreBackpressure) return;
m_outstandingTxnsSemaphore.release();
}
}
/**
*
*
* @param timestamp The time as measured when the call is made.
* @param ignoreBackpressure If true, never block.
* @return The time as measured when the call returns.
*/
long sendTxnWithOptionalBlockAndReturnCurrentTime(long timestampNanos, long timeoutNanos, boolean ignoreBackpressure) throws TimeoutException {
if (m_doesAnyTuning) {
long timestamp = TimeUnit.NANOSECONDS.toMillis(timestampNanos);
while (true) {
synchronized(this) {
// switch to a new block if 100ms has passed
// possibly compute a new target rate
ensureCurrentBlockIsKosher(timestamp);
assert((timestamp - m_currentBlockTimestamp) <= BLOCK_SIZE);
// don't let the time be before the start of the current block
// also ensure faketime - m_currentBlockTimestamp is positive
long faketime = timestamp < m_currentBlockTimestamp ? m_currentBlockTimestamp : timestamp;
long targetTxnsPerBlock = m_targetTxnsPerSecond / (1000 / BLOCK_SIZE);
// compute the percentage of the current 100ms block that has passed
double expectedTxnsSent =
targetTxnsPerBlock * (faketime - m_currentBlockTimestamp + 1.0) / BLOCK_SIZE;
expectedTxnsSent = Math.ceil(expectedTxnsSent);
assert(expectedTxnsSent <= targetTxnsPerBlock); // stupid fp math
assert((expectedTxnsSent >= 1.0) || (targetTxnsPerBlock == 0));
// if the rate is under target, no problems
if (((m_currentBlockSendCount < expectedTxnsSent) &&
(m_outstandingTxns < m_maxOutstandingTxns)) ||
(ignoreBackpressure == true)) {
// bookkeeping
++m_currentBlockSendCount;
++m_outstandingTxns;
// exit the while loop
break;
}
}
// if the rate is above target, pause for the smallest time possible
try { Thread.sleep(1); } catch (InterruptedException e) {}
timestampNanos = System.nanoTime();
timestamp = TimeUnit.NANOSECONDS.toMillis(timestampNanos);
}
} else {
if (ignoreBackpressure) return timestampNanos;
boolean acquired = m_outstandingTxnsSemaphore.tryAcquire();
if (!acquired) {
try {
if (!m_outstandingTxnsSemaphore.tryAcquire(timeoutNanos, TimeUnit.NANOSECONDS)) {
throw new TimeoutException();
}
} catch (InterruptedException e) {
Throwables.propagate(e);
}
return System.nanoTime();
}
}
// this time may have changed if this call blocked during a sleep
return timestampNanos;
}
public synchronized void debug() {
System.out.printf("Target throughput/s is %d and max outstanding txns is %d\n",
m_targetTxnsPerSecond, m_maxOutstandingTxns);
System.out.printf("Current outstanding is %d and recent internal latency is %.2f\n",
m_outstandingTxns, m_prevInternalLatencyAvgs.peekFirst());
}
}