fix(security): amortize LoginThrottle.Prune off the failed-bind hot path (plan R2-07 T11)
This commit is contained in:
@@ -41,6 +41,18 @@ public sealed class LoginThrottle
|
||||
/// </summary>
|
||||
public const int MaxTrackedKeys = 10_000;
|
||||
|
||||
/// <summary>Amortization cadence: a full prune sweep runs every Nth failure write
|
||||
/// (plus immediately whenever the map exceeds <see cref="MaxTrackedKeys"/>), so a
|
||||
/// spray does not pay an O(n) scan — and a possible full sort — on every failed
|
||||
/// request (arch-review R2 N8). Lockout SEMANTICS are unaffected: IsLockedOut reads
|
||||
/// window/lockout expiry directly and never depends on pruning.</summary>
|
||||
internal const int PruneEveryNFailures = 64;
|
||||
|
||||
/// <summary>Diagnostics/test accessor: number of currently tracked keys.</summary>
|
||||
internal int TrackedKeyCount => _entries.Count;
|
||||
|
||||
private int _failureWrites;
|
||||
|
||||
private readonly TimeProvider _clock;
|
||||
private readonly IOptions<SecurityOptions> _options;
|
||||
|
||||
@@ -118,7 +130,13 @@ public sealed class LoginThrottle
|
||||
return Arm(existing with { Count = existing.Count + 1 }, opts, now, lockout);
|
||||
});
|
||||
|
||||
Prune(now);
|
||||
// Amortized prune (R2 N8): every Nth failure, or immediately when over the hard
|
||||
// cap — the cap is the memory-safety invariant and must never wait for the cadence.
|
||||
if (Interlocked.Increment(ref _failureWrites) % PruneEveryNFailures == 0
|
||||
|| _entries.Count > MaxTrackedKeys)
|
||||
{
|
||||
Prune(now);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user