Add basic implementation of token bucket for flow-control (#3106)
This commit is contained in:
parent
74f7d184cf
commit
0c97d8f4b1
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright 1999-2023 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.alibaba.csp.sentinel.slots.block.flow.tokenbucket;
|
||||
|
||||
import com.alibaba.csp.sentinel.util.AssertUtil;
|
||||
import com.alibaba.csp.sentinel.util.TimeUtil;
|
||||
|
||||
/**
|
||||
* @author LearningGp
|
||||
*/
|
||||
public class AbstractTokenBucket implements TokenBucket{
|
||||
protected final long MAX_UNIT_PRODUCE_NUM = Long.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* Number of tokens left in the bucket
|
||||
*/
|
||||
protected volatile long currentTokenNum;
|
||||
|
||||
/**
|
||||
* Time of next production token
|
||||
*/
|
||||
protected volatile long nextProduceTime;
|
||||
|
||||
/**
|
||||
* Number of tokens produced per unit of time
|
||||
*/
|
||||
protected final long unitProduceNum;
|
||||
|
||||
/**
|
||||
* Maximum number of tokens stored in the bucket
|
||||
*/
|
||||
protected final long maxTokenNum;
|
||||
|
||||
protected final long intervalInMs;
|
||||
protected final long startTime;
|
||||
|
||||
public AbstractTokenBucket(long unitProduceNum, long maxTokenNum, boolean fullStart, long intervalInMs) {
|
||||
AssertUtil.isTrue(unitProduceNum > 0 && intervalInMs > 0 && unitProduceNum < MAX_UNIT_PRODUCE_NUM,
|
||||
"Illegal unitProduceNum or intervalInSeconds");
|
||||
AssertUtil.isTrue(maxTokenNum > 0, "Illegal maxTokenNum");
|
||||
this.unitProduceNum = unitProduceNum;
|
||||
this.maxTokenNum = maxTokenNum;
|
||||
this.intervalInMs = intervalInMs;
|
||||
this.startTime = TimeUtil.currentTimeMillis();
|
||||
this.nextProduceTime = startTime;
|
||||
if (fullStart) {
|
||||
this.currentTokenNum = maxTokenNum;
|
||||
} else {
|
||||
//The token will be filled when the first request arrives (including the initial token)
|
||||
this.currentTokenNum = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryConsume(long tokenNum) {
|
||||
if (tokenNum <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (tokenNum > maxTokenNum) {
|
||||
return false;
|
||||
}
|
||||
long currentTimestamp = TimeUtil.currentTimeMillis();
|
||||
refreshCurrentTokenNum(currentTimestamp);
|
||||
if (tokenNum <= currentTokenNum) {
|
||||
currentTokenNum -= tokenNum;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshCurrentTokenNum(long currentTimestamp) {
|
||||
if (nextProduceTime > currentTimestamp) {
|
||||
return;
|
||||
}
|
||||
currentTokenNum = Math.min(maxTokenNum, currentTokenNum + calProducedTokenNum(currentTimestamp));
|
||||
updateNextProduceTime(currentTimestamp);
|
||||
}
|
||||
|
||||
protected long calProducedTokenNum(long currentTimestamp) {
|
||||
if (nextProduceTime > currentTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
long nextRefreshUnitCount = (nextProduceTime - startTime) / intervalInMs;
|
||||
long currentUnitCount = (currentTimestamp - startTime) / intervalInMs;
|
||||
long unitCount = currentUnitCount - nextRefreshUnitCount + 1;
|
||||
return unitCount * unitProduceNum;
|
||||
}
|
||||
|
||||
protected void updateNextProduceTime(long currentTimestamp) {
|
||||
nextProduceTime = intervalInMs - ((currentTimestamp - startTime) % intervalInMs) + currentTimestamp;
|
||||
}
|
||||
|
||||
public long refreshTokenAndGetCurrentTokenNum() {
|
||||
refreshCurrentTokenNum(TimeUtil.currentTimeMillis());
|
||||
return currentTokenNum;
|
||||
}
|
||||
|
||||
public long getCurrentTokenNum() {
|
||||
return currentTokenNum;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 1999-2023 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.alibaba.csp.sentinel.slots.block.flow.tokenbucket;
|
||||
|
||||
/**
|
||||
* @author LearningGp
|
||||
*/
|
||||
public class DefaultTokenBucket extends AbstractTokenBucket{
|
||||
|
||||
public DefaultTokenBucket(long unitProduceNum, long maxTokenNum, long intervalInMs){
|
||||
super(unitProduceNum, maxTokenNum, false, intervalInMs);
|
||||
}
|
||||
|
||||
public DefaultTokenBucket(long unitProduceNum, long maxTokenNum, boolean fullStart, long intervalInMs){
|
||||
super(unitProduceNum, maxTokenNum, fullStart, intervalInMs);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 1999-2023 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.alibaba.csp.sentinel.slots.block.flow.tokenbucket;
|
||||
|
||||
import com.alibaba.csp.sentinel.util.TimeUtil;
|
||||
|
||||
/**
|
||||
* @author LearningGp
|
||||
*/
|
||||
public class StrictTokenBucket extends AbstractTokenBucket{
|
||||
|
||||
final private Object refreshLock = new Object();
|
||||
final private Object consumeLock = new Object();
|
||||
|
||||
public StrictTokenBucket(long unitProduceNum, long maxTokenNum, long intervalInMs) {
|
||||
super(unitProduceNum, maxTokenNum, false, intervalInMs);
|
||||
}
|
||||
|
||||
public StrictTokenBucket(long unitProduceNum, long maxTokenNum, boolean fullStart, long intervalInMs) {
|
||||
super(unitProduceNum, maxTokenNum, fullStart, intervalInMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryConsume(long tokenNum) {
|
||||
if (tokenNum > maxTokenNum) {
|
||||
return false;
|
||||
}
|
||||
long currentTimestamp = TimeUtil.currentTimeMillis();
|
||||
refreshCurrentTokenNum(currentTimestamp);
|
||||
if (tokenNum <= currentTokenNum) {
|
||||
synchronized (consumeLock) {
|
||||
if (tokenNum <= currentTokenNum) {
|
||||
currentTokenNum -= tokenNum;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshCurrentTokenNum(long currentTimestamp) {
|
||||
if (nextProduceTime > currentTimestamp) {
|
||||
return;
|
||||
}
|
||||
long producedTokenNum = calProducedTokenNum(currentTimestamp);
|
||||
synchronized (refreshLock) {
|
||||
if (nextProduceTime > currentTimestamp) {
|
||||
return;
|
||||
}
|
||||
currentTokenNum = Math.min(maxTokenNum, currentTokenNum + producedTokenNum);
|
||||
updateNextProduceTime(currentTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 1999-2023 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.alibaba.csp.sentinel.slots.block.flow.tokenbucket;
|
||||
|
||||
/**
|
||||
* @author LearningGp
|
||||
*/
|
||||
public interface TokenBucket {
|
||||
|
||||
boolean tryConsume(long tokenNum);
|
||||
|
||||
void refreshCurrentTokenNum(long timestamp);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright 1999-2023 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.alibaba.csp.sentinel.slots.block.flow.tokenbucket;
|
||||
|
||||
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
|
||||
import com.alibaba.csp.sentinel.test.AbstractTimeBasedTest;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author LearningGp
|
||||
*/
|
||||
public class TokenBucketTest extends AbstractTimeBasedTest {
|
||||
|
||||
private static ThreadPoolExecutor threadPoolExecutor;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() throws Exception {
|
||||
threadPoolExecutor = new ThreadPoolExecutor(64, 64, 0,
|
||||
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
|
||||
new NamedThreadFactory("sentinel-token-bucket-test", true),
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() throws Exception {
|
||||
threadPoolExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForDefaultTokenBucket() throws InterruptedException {
|
||||
long unitProduceNum = 1;
|
||||
long maxTokenNum = 2;
|
||||
long intervalInMs = 1000;
|
||||
long testStart = System.currentTimeMillis();
|
||||
setCurrentMillis(testStart);
|
||||
|
||||
DefaultTokenBucket defaultTokenBucket = new DefaultTokenBucket(unitProduceNum, maxTokenNum, intervalInMs);
|
||||
|
||||
assertTrue(defaultTokenBucket.tryConsume(1));
|
||||
assertFalse(defaultTokenBucket.tryConsume(1));
|
||||
|
||||
DefaultTokenBucket defaultTokenBucketFullStart = new DefaultTokenBucket(unitProduceNum, maxTokenNum,
|
||||
true, intervalInMs);
|
||||
|
||||
assertTrue(defaultTokenBucketFullStart.tryConsume(2));
|
||||
assertFalse(defaultTokenBucketFullStart.tryConsume(1));
|
||||
|
||||
sleep(1000);
|
||||
assertTrue(defaultTokenBucket.tryConsume(1));
|
||||
assertFalse(defaultTokenBucket.tryConsume(1));
|
||||
|
||||
sleep(1000);
|
||||
assertTrue(defaultTokenBucketFullStart.tryConsume(2));
|
||||
assertFalse(defaultTokenBucketFullStart.tryConsume(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForStrictTokenBucket() throws InterruptedException {
|
||||
long unitProduceNum = 5;
|
||||
long maxTokenNum = 10;
|
||||
long intervalInMs = 1000;
|
||||
final int n = 64;
|
||||
long testStart = System.currentTimeMillis();
|
||||
setCurrentMillis(testStart);
|
||||
|
||||
final AtomicLong passNum = new AtomicLong();
|
||||
final AtomicLong passNumFullStart = new AtomicLong();
|
||||
final CountDownLatch countDownLatch = new CountDownLatch(n);
|
||||
final CountDownLatch countDownLatchFullStart = new CountDownLatch(n);
|
||||
final StrictTokenBucket strictTokenBucket = new StrictTokenBucket(unitProduceNum, maxTokenNum, intervalInMs);
|
||||
final StrictTokenBucket strictTokenBucketFullStart = new StrictTokenBucket(unitProduceNum, maxTokenNum, true,
|
||||
intervalInMs);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
threadPoolExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (strictTokenBucket.tryConsume(1)) {
|
||||
passNum.incrementAndGet();
|
||||
}
|
||||
countDownLatch.countDown();
|
||||
}
|
||||
});
|
||||
threadPoolExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (strictTokenBucketFullStart.tryConsume(1)) {
|
||||
passNumFullStart.incrementAndGet();
|
||||
}
|
||||
countDownLatchFullStart.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
countDownLatch.await();
|
||||
countDownLatchFullStart.await();
|
||||
assertEquals(5, passNum.longValue());
|
||||
assertEquals(10, passNumFullStart.longValue());
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue