-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Description
_tokenCount is stored as double and may hold fractional values after replenishment. AttemptAcquire(0) returns a successful lease whenever _tokenCount > 0, yet GetStatistics().CurrentAvailablePermits truncates the same value to long, so it reports 0.
This means that AttemptAcquire(0) will always grants a lease, even when none should be given.
Reproduction
[Test]
public async Task FractionalTokenBug()
{
var limiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = 3,
TokensPerPeriod = 1,
ReplenishmentPeriod = TimeSpan.FromSeconds(0.5),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
AutoReplenishment = false
});
// Drain bucket
limiter.AttemptAcquire(3).Dispose();
await Task.Delay(500); // enough to add ~1.0 token
limiter.TryReplenish(); // _tokenCount ≈ 1.0
limiter.AttemptAcquire(1).Dispose(); // succeeds as expected
Assert.That(limiter.GetStatistics()?.CurrentAvailablePermits, Is.EqualTo(0), "Tokens after acquiring 1 permit");
var lease = limiter.AttemptAcquire(0); // **unexpected success**
Assert.That(!lease.IsAcquired, "Acquired a lease, when none should be available");
lease.Dispose();
}Resutls
Assert.That(!lease.IsAcquired, "Acquired a lease, when none should be available"); is failing.
Setting a break point, showed the value of _tokenCount to be 0.025640600000000013 in one example test run.
Root cause
GetStatistics()truncates with(long)_tokenCount, so shows as0.AttemptAcquireCoreconsiders any_tokenCount > 0a success, even when_tokenCountsits in(0,1)
The cast hides the available fraction from statistics but the acquisition path still sees it, there are multiple places in the class that should be updated to _tokenCount >= 1, or where conditions are checking for _tokenCount == 0, that will never happen once the first TryReplenish triggers.