From 099b1ac0d0f9f46ee0765f686fc5efaea8c2e691 Mon Sep 17 00:00:00 2001 From: "vladimir.bukhtoyarov" Date: Sun, 14 Nov 2021 18:45:46 +0300 Subject: [PATCH] #187 backport calculateFullRefillingTime to the Verbose API --- .../java/io/github/bucket4j/BucketState.java | 26 ++++- .../io/github/bucket4j/VerboseResult.java | 28 +++++- .../io/github/bucket4j/VerboseApiTest.groovy | 97 +++++++++++++++++++ 3 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 bucket4j-core/src/test/java/io/github/bucket4j/VerboseApiTest.groovy diff --git a/bucket4j-core/src/main/java/io/github/bucket4j/BucketState.java b/bucket4j-core/src/main/java/io/github/bucket4j/BucketState.java index 931d196b..c95a1700 100644 --- a/bucket4j-core/src/main/java/io/github/bucket4j/BucketState.java +++ b/bucket4j-core/src/main/java/io/github/bucket4j/BucketState.java @@ -478,7 +478,7 @@ private void setLastRefillTimeNanos(int bandwidth, long nanos) { stateData[bandwidth * BANDWIDTH_SIZE] = nanos; } - long getCurrentSize(int bandwidth) { + public long getCurrentSize(int bandwidth) { return stateData[bandwidth * BANDWIDTH_SIZE + 1]; } @@ -490,7 +490,7 @@ private void consume(int bandwidth, long tokens) { stateData[bandwidth * BANDWIDTH_SIZE + 1] -= tokens; } - long getRoundingError(int bandwidth) { + public long getRoundingError(int bandwidth) { return stateData[bandwidth * BANDWIDTH_SIZE + 2]; } @@ -498,6 +498,28 @@ private void setRoundingError(int bandwidth, long roundingError) { stateData[bandwidth * BANDWIDTH_SIZE + 2] = roundingError; } + public long calculateFullRefillingTime(Bandwidth[] bandwidths, long currentTimeNanos) { + long maxTimeToFullRefillNanos = calculateFullRefillingTime(0, bandwidths[0], currentTimeNanos); + for (int i = 1; i < bandwidths.length; i++) { + maxTimeToFullRefillNanos = Math.max(maxTimeToFullRefillNanos, calculateFullRefillingTime(i, bandwidths[i], currentTimeNanos)); + } + return maxTimeToFullRefillNanos; + } + + private long calculateFullRefillingTime(int bandwidthIndex, Bandwidth bandwidth, long currentTimeNanos) { + long availableTokens = getCurrentSize(bandwidthIndex); + if (availableTokens >= bandwidth.capacity) { + return 0L; + } + long deficit = bandwidth.capacity - availableTokens; + + if (bandwidth.isRefillIntervally()) { + return calculateDelayNanosAfterWillBePossibleToConsumeForIntervalBandwidth(bandwidthIndex, bandwidth, deficit, currentTimeNanos); + } else { + return calculateDelayNanosAfterWillBePossibleToConsumeForGreedyBandwidth(bandwidthIndex, bandwidth, deficit); + } + } + public static final SerializationHandle SERIALIZATION_HANDLE = new SerializationHandle() { @Override public BucketState deserialize(DeserializationAdapter adapter, S input) throws IOException { diff --git a/bucket4j-core/src/main/java/io/github/bucket4j/VerboseResult.java b/bucket4j-core/src/main/java/io/github/bucket4j/VerboseResult.java index cfbdc354..bdbb72b0 100644 --- a/bucket4j-core/src/main/java/io/github/bucket4j/VerboseResult.java +++ b/bucket4j-core/src/main/java/io/github/bucket4j/VerboseResult.java @@ -38,6 +38,12 @@ public class VerboseResult implements Serializable { private final T value; private final BucketConfiguration configuration; private final BucketState state; + private final Diagnostics diagnostics = new Diagnostics() { + @Override + public long calculateFullRefillingTime() { + return state.calculateFullRefillingTime(configuration.getBandwidths(), operationTimeNanos); + } + }; public VerboseResult(long operationTimeNanos, T value, BucketConfiguration configuration, BucketState state) { this.operationTimeNanos = operationTimeNanos; @@ -53,7 +59,6 @@ public T getValue() { return value; } - /** * @return snapshot of configuration which was actual at operation time */ @@ -75,6 +80,27 @@ public long getOperationTimeNanos() { return operationTimeNanos; } + /** + * @return internal state describer + */ + public Diagnostics getDiagnostics() { + return diagnostics; + } + + /** + * Describer of internal bucket state + */ + interface Diagnostics { + + /** + * Returns time in nanoseconds that need to wait until bucket will be fully refilled to its maximum + * + * @return time in nanoseconds that need to wait until bucket will be fully refilled to its maximum + */ + long calculateFullRefillingTime(); + + } + public VerboseResult map(Function mapper) { return new VerboseResult<>(operationTimeNanos, mapper.apply(value), configuration, state); } diff --git a/bucket4j-core/src/test/java/io/github/bucket4j/VerboseApiTest.groovy b/bucket4j-core/src/test/java/io/github/bucket4j/VerboseApiTest.groovy new file mode 100644 index 00000000..431096eb --- /dev/null +++ b/bucket4j-core/src/test/java/io/github/bucket4j/VerboseApiTest.groovy @@ -0,0 +1,97 @@ +package io.github.bucket4j + +import spock.lang.Specification +import spock.lang.Unroll + +import java.time.Duration + +class VerboseApiTest extends Specification { + + @Unroll + def "calculateFullRefillingTime specification #testNumber"(String testNumber, long requiredTime, + long timeShiftBeforeAsk, long tokensConsumeBeforeAsk, BucketConfiguration configuration) { + setup: + long currentTimeNanos = 0L + BucketState state = BucketState.createInitialState(configuration, currentTimeNanos) + state.refillAllBandwidth(configuration.bandwidths, timeShiftBeforeAsk) + state.consume(configuration.bandwidths, tokensConsumeBeforeAsk) + VerboseResult verboseResult = new VerboseResult(timeShiftBeforeAsk, 42, configuration, state) + + when: + long actualTime = verboseResult.getDiagnostics().calculateFullRefillingTime() + then: + actualTime == requiredTime + where: + [testNumber, requiredTime, timeShiftBeforeAsk, tokensConsumeBeforeAsk, configuration] << [ + [ + "#1", + 90, + 0, + 0, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.simple(10, Duration.ofNanos(100)).withInitialTokens(1)) + .build() + ], [ + "#2", + 100, + 0, + 0, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofNanos(100))).withInitialTokens(1)) + .build() + ], [ + "#3", + 1650, + 0, + 23, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.classic(10, Refill.greedy(2, Duration.ofNanos(100))).withInitialTokens(0)) + .build() + ], [ + "#4", + 1700, + 0, + 23, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.classic(10, Refill.intervally(2, Duration.ofNanos(100))).withInitialTokens(0)) + .build() + ], [ + "#5", + 60, + 0, + 0, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.simple(10, Duration.ofNanos(100)).withInitialTokens(4)) + .build() + ], [ + "#6", + 90, + 0, + 0, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.simple(10, Duration.ofNanos(100)).withInitialTokens(1)) + .addLimit(Bandwidth.simple(5, Duration.ofNanos(10)).withInitialTokens(2)) + .build() + ], [ + "#7", + 90, + 0, + 0, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.simple(5, Duration.ofNanos(10)).withInitialTokens(2)) + .addLimit(Bandwidth.simple(10, Duration.ofNanos(100)).withInitialTokens(1)) + .build() + ], [ + "#8", + 70, + 0, + 0, + Bucket4j.configurationBuilder() + .addLimit(Bandwidth.simple(5, Duration.ofNanos(10)).withInitialTokens(5)) + .addLimit(Bandwidth.simple(10, Duration.ofNanos(100)).withInitialTokens(3)) + .build() + ] + ] + } + +}