RetryBudget: swap Reset() write order to avoid stale-tick race

Previous Reset():
  Volatile.Write(ref m_started, 0);
  Interlocked.Exchange(ref m_startTicks, kNotStarted);

A concurrent EnsureStarted() observer could see m_started == 0 (taking
the "reset, so go start" branch), then read the stale m_startTicks
value BEFORE the Interlocked.Exchange landed, and treat the budget as
already-started against the pre-reset timestamp. IsExhausted /
TryConsume then computed elapsed time from the old origin.

Fix: reset m_startTicks first, then m_started. EnsureStarted writes
m_started = 1 only after the CAS on m_startTicks succeeds, so
resetting m_startTicks first preserves that invariant — no observer
can see "started == 1" with a stale tick value.
This commit is contained in:
Marc Schier
2026-06-11 09:56:06 +02:00
parent 2a4f4f6014
commit d0ef235286
@@ -121,8 +121,12 @@ namespace Opc.Ua
/// <inheritdoc/>
public void Reset()
{
Volatile.Write(ref m_started, 0);
// Order matters: clear m_startTicks BEFORE m_started so a concurrent
// EnsureStarted observer cannot see "started=1" with a stale tick value.
// EnsureStarted writes m_started=1 only after the CAS on m_startTicks
// succeeds, so resetting m_startTicks first preserves that invariant.
Interlocked.Exchange(ref m_startTicks, kNotStarted);
Volatile.Write(ref m_started, 0);
}
private long EnsureStarted()