fix(driver.sql): compare-and-swap the health field so a concurrent transition cannot be lost (Driver.Sql-010)
This commit is contained in:
@@ -356,6 +356,55 @@ public sealed class SqlDriverTests
|
||||
driver.GetHealth().LastSuccessfulRead.ShouldBe(before);
|
||||
}
|
||||
|
||||
// ---- Driver.Sql-010: the health field is read-modify-written from several poll loops ----
|
||||
|
||||
[Fact]
|
||||
public async Task HealthTransitions_fromConcurrentPolls_neverRewindTheEvidenceClock()
|
||||
{
|
||||
// Driver.Sql-010. Every health transition is a read-modify-write of one field: Degrade reads the
|
||||
// current snapshot to PRESERVE LastSuccessfulRead, then writes a new record. With a plain
|
||||
// Volatile read + write, a Degrade that read the clock at t0 can be published after a concurrent
|
||||
// advance to t1 — so the clock goes BACKWARDS, and the advance is lost. That is not cosmetic: the
|
||||
// comms-loss watchdog measures how long the clock has been frozen, so a lost advance is a spurious
|
||||
// 90 s reconnect. Monotonicity is the observable the lost update violates, and it is exactly what a
|
||||
// compare-and-swap guarantees.
|
||||
//
|
||||
// A driver with several subscriptions runs one poll loop each, plus any ad-hoc ReadAsync — so two
|
||||
// classifications landing at once is the ordinary case, not a contrived one.
|
||||
using var fixture = new SqlitePollFixture();
|
||||
await using var driver = NewDriver(fixture, KvEntry("Speed", SqlitePollFixture.PresentKey));
|
||||
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
|
||||
|
||||
const int iterations = 30_000;
|
||||
var advancing = new DataValueSnapshot(1.0, SqlStatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow);
|
||||
var degrading = new DataValueSnapshot(null, SqlStatusCodes.BadTimeout, DateTime.UtcNow, DateTime.UtcNow);
|
||||
using var done = new CancellationTokenSource();
|
||||
|
||||
var rewinds = 0;
|
||||
var observer = Task.Run(() =>
|
||||
{
|
||||
var highWater = driver.GetHealth().LastSuccessfulRead ?? DateTime.MinValue;
|
||||
while (!done.IsCancellationRequested)
|
||||
{
|
||||
var seen = driver.GetHealth().LastSuccessfulRead ?? DateTime.MinValue;
|
||||
if (seen < highWater) Interlocked.Increment(ref rewinds);
|
||||
else highWater = seen;
|
||||
}
|
||||
});
|
||||
|
||||
var writers = new[]
|
||||
{
|
||||
Task.Run(() => { for (var i = 0; i < iterations; i++) driver.ObservePollOutcome([advancing]); }),
|
||||
Task.Run(() => { for (var i = 0; i < iterations; i++) driver.ObservePollOutcome([degrading]); }),
|
||||
};
|
||||
|
||||
await Task.WhenAll(writers);
|
||||
await done.CancelAsync();
|
||||
await observer;
|
||||
|
||||
Volatile.Read(ref rewinds).ShouldBe(0);
|
||||
}
|
||||
|
||||
// ---- ISubscribable (poll-engine overlay) ----
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user