1515 */
1616package org .jivesoftware .util ;
1717
18+ import com .google .common .annotations .VisibleForTesting ;
19+
1820import java .time .Duration ;
21+ import java .util .Objects ;
22+ import java .util .function .LongSupplier ;
1923
2024/**
2125 * A thread-safe, synchronized token-bucket rate limiter with metrics.
@@ -29,9 +33,12 @@ public final class TokenBucketRateLimiter
2933{
3034 private final long capacity ;
3135 private final long refillTokensPerSecond ;
36+ private final LongSupplier nanoTimeSupplier ;
3237
3338 private long availableTokens ;
3439 private long lastRefillTimeNanos ;
40+ // Leftover refill value in scaled units (1 token = 1_000_000_000 units).
41+ private long refillRemainder ;
3542
3643 private long acceptedEvents ;
3744 private long rejectedEvents ;
@@ -46,14 +53,16 @@ public final class TokenBucketRateLimiter
4653 /**
4754 * Creates an unlimited rate limiter. Use {@link #unlimited()} to obtain an instance.
4855 */
49- private TokenBucketRateLimiter ()
56+ private TokenBucketRateLimiter (final LongSupplier nanoTimeSupplier )
5057 {
58+ this .nanoTimeSupplier = Objects .requireNonNull (nanoTimeSupplier , "nanoTimeSupplier must not be null" );
5159 this .unlimited = true ;
5260 this .refillTokensPerSecond = 0 ;
5361 this .capacity = 0 ;
5462 this .availableTokens = 0 ;
5563 this .lastRefillTimeNanos = 0 ;
56- this .startTimeNanos = System .nanoTime ();
64+ this .refillRemainder = 0 ;
65+ this .startTimeNanos = nanoTime ();
5766 }
5867
5968 /**
@@ -63,6 +72,21 @@ private TokenBucketRateLimiter()
6372 * @param maxBurst maximum number of permits that can accumulate
6473 */
6574 public TokenBucketRateLimiter (final long permitsPerSecond , final long maxBurst )
75+ {
76+ this (permitsPerSecond , maxBurst , System ::nanoTime );
77+ }
78+
79+ /**
80+ * Creates a new rate limiter with a custom clock.
81+ *
82+ * Normally, the system clock is used. This constructor mainly exists for tests.
83+ *
84+ * @param permitsPerSecond sustained rate of permits
85+ * @param maxBurst maximum number of permits that can accumulate
86+ * @param nanoTimeSupplier custom clock
87+ */
88+ @ VisibleForTesting
89+ TokenBucketRateLimiter (final long permitsPerSecond , final long maxBurst , final LongSupplier nanoTimeSupplier )
6690 {
6791 if (permitsPerSecond <= 0 ) {
6892 throw new IllegalArgumentException ("permitsPerSecond must be > 0" );
@@ -71,11 +95,13 @@ public TokenBucketRateLimiter(final long permitsPerSecond, final long maxBurst)
7195 throw new IllegalArgumentException ("maxBurst must be > 0" );
7296 }
7397
98+ this .nanoTimeSupplier = Objects .requireNonNull (nanoTimeSupplier , "nanoTimeSupplier must not be null" );
7499 this .unlimited = false ;
75100 this .refillTokensPerSecond = permitsPerSecond ;
76101 this .capacity = maxBurst ;
77102 this .availableTokens = maxBurst ;
78- this .lastRefillTimeNanos = System .nanoTime ();
103+ this .lastRefillTimeNanos = nanoTime ();
104+ this .refillRemainder = 0 ;
79105 this .startTimeNanos = this .lastRefillTimeNanos ;
80106 }
81107
@@ -86,7 +112,19 @@ public TokenBucketRateLimiter(final long permitsPerSecond, final long maxBurst)
86112 */
87113 public static TokenBucketRateLimiter unlimited ()
88114 {
89- return new TokenBucketRateLimiter ();
115+ return new TokenBucketRateLimiter (System ::nanoTime );
116+ }
117+
118+ /**
119+ * Returns the current time in nanoseconds from this instance clock.
120+ *
121+ * Usually this is the system clock, but tests can provide a custom clock.
122+ *
123+ * @return The time in nanoseconds.
124+ */
125+ private long nanoTime ()
126+ {
127+ return nanoTimeSupplier .getAsLong ();
90128 }
91129
92130 /**
@@ -112,30 +150,54 @@ public synchronized boolean tryAcquire()
112150 }
113151
114152 /**
115- * Refills tokens based on elapsed time.
153+ * Adds tokens based on elapsed time.
116154 */
117155 private void refillIfNeeded ()
118156 {
119- final long now = System . nanoTime ();
157+ final long now = nanoTime ();
120158 final long elapsed = now - lastRefillTimeNanos ;
159+ // With very small intervals, no time may have passed yet.
121160 if (elapsed <= 0 ) {
122161 return ;
123162 }
124163
125- // If the multiplication would overflow, elapsed time is so large that the bucket would be completely refilled
126- // regardless, so cap directly at capacity.
127- final long tokensToAdd ;
128- if (elapsed > Long .MAX_VALUE / refillTokensPerSecond ) {
129- tokensToAdd = capacity ;
130- } else {
131- tokensToAdd = Math .min (capacity , (elapsed * refillTokensPerSecond ) / 1_000_000_000L );
164+ if (availableTokens >= capacity ) {
165+ // When already full, do not store extra time as hidden credit.
166+ lastRefillTimeNanos = now ;
167+ refillRemainder = 0 ;
168+ return ;
132169 }
133170
134- if (tokensToAdd > 0 ) {
135- availableTokens = tokensToAdd >= capacity - availableTokens ? capacity : availableTokens + tokensToAdd ;
136- // Only advance the timestamp when tokens are actually added, so that sub-token elapsed time is preserved
137- // and contributes to the next refill rather than being discarded.
171+ final long remainingCapacity = capacity - availableTokens ;
172+
173+ // Refill is calculated in scaled integer units: (elapsed * rate) + previous leftover.
174+ // If this overflows, elapsed time is so large that the bucket must be full.
175+ if (elapsed > (Long .MAX_VALUE - refillRemainder ) / refillTokensPerSecond ) {
176+ availableTokens = capacity ;
177+ lastRefillTimeNanos = now ;
178+ refillRemainder = 0 ;
179+ return ;
180+ }
181+
182+ // Convert elapsed time to scaled units (1_000_000_000 units = 1 token).
183+ final long refillUnits = elapsed * refillTokensPerSecond + refillRemainder ;
184+ final long tokensToGenerate = refillUnits / 1_000_000_000L ;
185+ // Keep leftover units until they become at least one full token.
186+ if (tokensToGenerate <= 0 ) {
187+ return ;
188+ }
189+
190+ final long tokensToAdd = Math .min (remainingCapacity , tokensToGenerate );
191+ availableTokens += tokensToAdd ;
192+
193+ if (availableTokens >= capacity ) {
194+ // Once capacity is reached, drop any extra accrued value.
195+ lastRefillTimeNanos = now ;
196+ refillRemainder = 0 ;
197+ } else {
198+ // Keep the leftover fraction so refill speed stays accurate over time.
138199 lastRefillTimeNanos = now ;
200+ refillRemainder = refillUnits % 1_000_000_000L ;
139201 }
140202 }
141203
@@ -220,7 +282,7 @@ public synchronized double getAcceptanceRatio() {
220282 * @return uptime duration
221283 */
222284 public Duration getUptime () {
223- return Duration .ofNanos (System . nanoTime () - startTimeNanos );
285+ return Duration .ofNanos (nanoTime () - startTimeNanos );
224286 }
225287
226288 /**
0 commit comments