Pin the budget sweep and Retry-After rounding; drop a redundant Math.Max
Survivors of the first Stryker pass on FixedWindowBudget: the sweep dropping live windows, sweeping every call, and the exact window boundary (masked by the sweep in the first test). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7f375ecf8f
commit
9fcef927fd
3 changed files with 45 additions and 3 deletions
|
|
@ -92,14 +92,15 @@ public sealed class ConceptVoteRateLimitTests : IClassFixture<ConceptVoteRateLim
|
|||
Assert.Equal("600", RetryAfter(first));
|
||||
}
|
||||
|
||||
_factory.Clock.Advance(TimeSpan.FromSeconds(100));
|
||||
// 499.5 s remain: Retry-After rounds up, so a client told to wait never retries early.
|
||||
_factory.Clock.Advance(TimeSpan.FromMilliseconds(100_500));
|
||||
using (var later = await device.Post(collection, "Up"))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.TooManyRequests, later.StatusCode);
|
||||
Assert.Equal("500", RetryAfter(later));
|
||||
}
|
||||
|
||||
_factory.Clock.Advance(TimeSpan.FromSeconds(499));
|
||||
_factory.Clock.Advance(TimeSpan.FromMilliseconds(498_500));
|
||||
using (var last = await device.Post(collection, "Up"))
|
||||
{
|
||||
Assert.Equal(HttpStatusCode.TooManyRequests, last.StatusCode);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,47 @@ public sealed class FixedWindowBudgetTests
|
|||
Assert.True(budget.TryAcquire("a", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_sweep_drops_only_expired_windows_and_a_live_one_keeps_counting_to_its_exact_end()
|
||||
{
|
||||
var clock = new Clock();
|
||||
var budget = new FixedWindowBudget(1, Window, clock);
|
||||
budget.TryAcquire("a", out _);
|
||||
clock.Advance(TimeSpan.FromSeconds(30));
|
||||
budget.TryAcquire("b", out _);
|
||||
clock.Advance(TimeSpan.FromSeconds(30));
|
||||
|
||||
// The sweep due at 60 s drops a, which ended, and keeps b, which has 30 s left.
|
||||
budget.TryAcquire("c", out _);
|
||||
Assert.Equal(2, budget.TrackedKeys);
|
||||
Assert.False(budget.TryAcquire("b", out var wait));
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), wait);
|
||||
|
||||
clock.Advance(TimeSpan.FromSeconds(30) - TimeSpan.FromTicks(1));
|
||||
Assert.False(budget.TryAcquire("b", out _));
|
||||
clock.Advance(TimeSpan.FromTicks(1));
|
||||
Assert.True(budget.TryAcquire("b", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sweeping_is_at_most_once_per_window_so_a_flood_of_keys_costs_one_pass_not_one_per_request()
|
||||
{
|
||||
var clock = new Clock();
|
||||
var budget = new FixedWindowBudget(1, Window, clock);
|
||||
budget.TryAcquire("a", out _);
|
||||
clock.Advance(TimeSpan.FromSeconds(30));
|
||||
budget.TryAcquire("b", out _);
|
||||
clock.Advance(TimeSpan.FromSeconds(31));
|
||||
budget.TryAcquire("c", out _);
|
||||
Assert.Equal(2, budget.TrackedKeys);
|
||||
|
||||
// b ended at 90 s but the next sweep is not due until 121 s.
|
||||
clock.Advance(TimeSpan.FromSeconds(39));
|
||||
budget.TryAcquire("d", out _);
|
||||
|
||||
Assert.Equal(3, budget.TrackedKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Expired_windows_are_dropped_so_the_table_does_not_grow_with_every_key_ever_seen()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue