/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.restli.throttling; import java.util.concurrent.TimeUnit; import com.google.common.base.Preconditions; import lombok.AccessLevel; import lombok.Getter; /** * An implementation of Token Bucket (https://en.wikipedia.org/wiki/Token_bucket). * * This class is intended to limit the rate at which tokens are used to a given QPS. It can store tokens for future usage. */ public class TokenBucket { @Getter(AccessLevel.PROTECTED) private double tokensPerMilli; private double maxBucketSizeInTokens; private volatile long nextTokenAvailableMillis; private volatile double tokensStored; public TokenBucket(long qps, long maxBucketSizeInMillis) { this.nextTokenAvailableMillis = System.currentTimeMillis(); resetQPS(qps, maxBucketSizeInMillis); } public void resetQPS(long qps, long maxBucketSizeInMillis) { Preconditions.checkArgument(qps > 0, "QPS must be positive."); Preconditions.checkArgument(maxBucketSizeInMillis >= 0, "Max bucket size must be non-negative."); long now = System.currentTimeMillis(); synchronized (this) { updateTokensStored(now); if (this.nextTokenAvailableMillis > now) { this.tokensStored -= (this.nextTokenAvailableMillis - now) * this.tokensPerMilli; } this.tokensPerMilli = (double) qps / 1000; this.maxBucketSizeInTokens = this.tokensPerMilli * maxBucketSizeInMillis; } } /** * Attempt to get the specified amount of tokens within the specified timeout. If the tokens cannot be retrieved in the * specified timeout, the call will return false immediately, otherwise, the call will block until the tokens are available. * * @return true if the tokens are granted. * @throws InterruptedException */ public boolean getTokens(long tokens, long timeout, TimeUnit timeoutUnit) throws InterruptedException { long timeoutMillis = timeoutUnit.toMillis(timeout); long wait; synchronized (this) { wait = tryReserveTokens(tokens, timeoutMillis); } if (wait < 0) { return false; } if (wait == 0) { return true; } Thread.sleep(wait); return true; } /** * Get the current number of stored tokens. Note this is a snapshot of the object, and there is no guarantee that those * tokens will be available at any point in the future. */ public long getStoredTokens() { synchronized (this) { updateTokensStored(System.currentTimeMillis()); } return (long) this.tokensStored; } /** * Note: this method should only be called while holding the class lock. For performance, the lock is not explicitly * acquired. * * @return the wait until the tokens are available or negative if they can't be acquired in the give timeout. */ private long tryReserveTokens(long tokens, long maxWaitMillis) { long now = System.currentTimeMillis(); long waitUntilNextTokenAvailable = Math.max(0, this.nextTokenAvailableMillis - now); updateTokensStored(now); if (tokens <= this.tokensStored) { this.tokensStored -= tokens; return waitUntilNextTokenAvailable; } double additionalNeededTokens = tokens - this.tokensStored; // casting to long will round towards 0 long additionalWaitForEnoughTokens = (long) (additionalNeededTokens / this.tokensPerMilli) + 1; long totalWait = waitUntilNextTokenAvailable + additionalWaitForEnoughTokens; if (totalWait > maxWaitMillis) { return -1; } this.tokensStored = this.tokensPerMilli * additionalWaitForEnoughTokens - additionalNeededTokens; this.nextTokenAvailableMillis = this.nextTokenAvailableMillis + additionalWaitForEnoughTokens; return totalWait; } /** * Note: this method should only be called while holding the class lock. For performance, the lock is not explicitly * acquired. */ private void updateTokensStored(long now) { if (now <= this.nextTokenAvailableMillis) { return; } long millisUnaccounted = now - this.nextTokenAvailableMillis; double newTokens = millisUnaccounted * this.tokensPerMilli; this.nextTokenAvailableMillis = now; this.tokensStored = Math.min(this.tokensStored + newTokens, Math.max(this.tokensStored, this.maxBucketSizeInTokens)); } }