fix(acl): make InvalidateIfStale a monotonic-max CAS loop — stamp publication is never deferred
This commit is contained in:
@@ -429,6 +429,8 @@ public sealed class SessionAclDecisionCache
|
||||
|
||||
/// <summary>
|
||||
/// Advances the cache-wide stamp when this call presents a newer generation, and purges on the way.
|
||||
/// The stamp ends up holding the highest generation any caller has presented — never a lower one,
|
||||
/// and never one call late.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -443,21 +445,54 @@ public sealed class SessionAclDecisionCache
|
||||
/// carries its own generation; what is at stake here is only whether the cache is any use.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ⚠️ <b>That guard lives INSIDE the loop, against a freshly re-read stamp</b> — hoisting it to a
|
||||
/// single check before the loop reintroduces exactly the backward roll it exists to prevent: a
|
||||
/// straggler that lost its exchange would spin back round and re-attempt against the newer stamp
|
||||
/// it just lost to, and eventually succeed in rolling it back. Re-reading means the straggler
|
||||
/// sees the newer value on its second pass and leaves.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Why a loop rather than one exchange.</b> A single shot defers publication by a call: two
|
||||
/// callers read the same baseline, the older one's exchange lands first, and the newer one's
|
||||
/// fails and falls out having published nothing. Its own lookup then misses and the
|
||||
/// <see cref="Set"/> that follows is dropped by the <c>Generation != generation</c> guard, which
|
||||
/// costs a store in the post-deploy window where the cache is coldest. Retrying against the moved
|
||||
/// stamp closes that: the higher generation is published on the call that presents it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The elected clear survives the loop.</b> Only the thread whose exchange actually moved the
|
||||
/// stamp clears, so a burst of concurrent requests arriving just after a deploy still repopulates
|
||||
/// once rather than each clearing the others' fresh entries. A retry can now clear too, but only
|
||||
/// by winning a move to a strictly higher generation — so clears stay bounded by the number of
|
||||
/// distinct generations presented, not by request volume.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It terminates.</b> Each iteration either returns through the guard or exchanges against a
|
||||
/// <c>seen</c> strictly larger than the last iteration's (a failed exchange means someone raised
|
||||
/// the stamp), and <c>_generation</c> only ever rises — so the guard is reached within a bounded
|
||||
/// number of passes.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The straggler's own lookup is unaffected by returning early: its shard-generation check misses
|
||||
/// against the newer shards regardless, so it recomputes, which is the correct outcome for a
|
||||
/// request holding a superseded snapshot. <c>_generation</c> starts at <c>-1</c>, below any real
|
||||
/// generation, so the first one still passes.
|
||||
/// generation, so the first one — generation <c>0</c> included — still passes.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="generation">The generation the calling request is being decided under.</param>
|
||||
private void InvalidateIfStale(long generation)
|
||||
{
|
||||
var seen = Interlocked.Read(ref _generation);
|
||||
if (generation <= seen) return;
|
||||
while (true)
|
||||
{
|
||||
var seen = Interlocked.Read(ref _generation);
|
||||
if (generation <= seen) return;
|
||||
|
||||
// Only the thread that actually moves the stamp clears, so a burst of concurrent requests arriving
|
||||
// just after a deploy repopulates once rather than each clearing the others' fresh entries.
|
||||
if (Interlocked.CompareExchange(ref _generation, generation, seen) == seen) ClearAll();
|
||||
if (Interlocked.CompareExchange(ref _generation, generation, seen) == seen)
|
||||
{
|
||||
ClearAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user