fix(driver.sql): advance LastSuccessfulRead on evidence the database answered (Driver.Sql-006)

This commit is contained in:
Joseph Doherty
2026-08-10 08:02:49 -04:00
parent ee1f1b28e8
commit a0a49d3395
2 changed files with 136 additions and 4 deletions
@@ -269,6 +269,93 @@ public sealed class SqlDriverTests
driver.GetHostStatuses()[0].State.ShouldBe(HostState.Running);
}
// ---- the comms-loss watchdog's evidence clock (Driver.Sql-006) ----
[Fact]
public async Task ReadAsync_whenTheDatabaseAnswersWithNoRow_advancesTheEvidenceClock_withoutChangingHealth()
{
// Driver.Sql-006. LastSuccessfulRead is what DriverInstanceActor's comms-loss watchdog reads: a
// non-Healthy driver whose clock has been frozen for 90 s is force-reconnected (Gitea #527), on the
// premise that the clock is the honest evidence the transport works. BadNoData IS that evidence —
// the query ran and the row is absent — so a driver whose key set was deleted must not be
// reconnected every 90 s against a database answering every poll on time. The health STATE stays
// exactly as it was: an absent row is a data fact, not a connectivity one.
using var fixture = new SqlitePollFixture();
await using var driver = NewDriver(fixture, KvEntry("Missing", SqlitePollFixture.AbsentKey));
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
// Something has degraded the driver — the watchdog's first conjunct. Only then does the frozen clock
// matter, which is why the scenario needs both.
driver.HandlePollError(new InvalidOperationException("Reader contract violation: expected 1 snapshots"));
var before = driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
await Task.Delay(20, TestContext.Current.CancellationToken);
var snapshots = await driver.ReadAsync(["Missing"], CancellationToken.None);
snapshots[0].StatusCode.ShouldBe(SqlStatusCodes.BadNoData);
var health = driver.GetHealth();
health.LastSuccessfulRead.ShouldNotBeNull().ShouldBeGreaterThan(before); // RED pre-fix: frozen
health.State.ShouldBe(DriverState.Degraded); // classification untouched
}
[Fact]
public async Task ReadAsync_anUnresolvableRef_isNoEvidenceTheDatabaseAnswered_soTheEvidenceClockStands()
{
// The falsifiability control: "advance on anything that is not Good" would pass the test above and
// break the watchdog outright. BadNodeIdUnknown is minted by the resolver without any database
// contact at all, so it proves nothing about the transport and must leave the clock alone —
// otherwise a driver polling only typoed refs would look forever alive to the watchdog.
using var fixture = new SqlitePollFixture();
await using var driver = NewDriver(fixture, KvEntry("Speed", SqlitePollFixture.PresentKey));
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
var before = driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
await Task.Delay(20, TestContext.Current.CancellationToken);
var snapshots = await driver.ReadAsync(["TypoedTagName"], CancellationToken.None);
snapshots[0].StatusCode.ShouldBe(SqlStatusCodes.BadNodeIdUnknown);
driver.GetHealth().LastSuccessfulRead.ShouldBe(before);
}
[Theory]
[InlineData(SqlStatusCodes.Good)] // the row is there and readable
[InlineData(SqlStatusCodes.BadNoData)] // the query ran; the row is absent
[InlineData(SqlStatusCodes.Uncertain)] // a present row with a NULL cell (nullIsBad=false)
[InlineData(SqlStatusCodes.Bad)] // …the same row under nullIsBad=true
[InlineData(SqlStatusCodes.BadTypeMismatch)] // a cell came back and would not coerce
[InlineData(SqlStatusCodes.BadCommunicationError)] // the connection opened; the query was refused
public async Task ObservePollOutcome_everyEvidenceOfAnswerCode_advancesTheEvidenceClock(uint statusCode)
{
using var fixture = new SqlitePollFixture();
await using var driver = NewDriver(fixture, KvEntry("Speed", SqlitePollFixture.PresentKey));
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
var before = driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
await Task.Delay(20, TestContext.Current.CancellationToken);
driver.ObservePollOutcome([new DataValueSnapshot(null, statusCode, DateTime.UtcNow, DateTime.UtcNow)]);
driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull().ShouldBeGreaterThan(before);
}
[Theory]
[InlineData(SqlStatusCodes.BadTimeout)] // the database did not answer at all
[InlineData(SqlStatusCodes.BadNodeIdUnknown)] // the resolver never reached the database
[InlineData(SqlStatusCodes.BadConfigurationError)] // the planner rejected the tag; nothing was executed
[InlineData(SqlStatusCodes.BadInternalError)] // a reader defect; no statement about the database
public async Task ObservePollOutcome_aCodeThatIsNoEvidenceOfAnAnswer_leavesTheEvidenceClockAlone(
uint statusCode)
{
using var fixture = new SqlitePollFixture();
await using var driver = NewDriver(fixture, KvEntry("Speed", SqlitePollFixture.PresentKey));
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
var before = driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
await Task.Delay(20, TestContext.Current.CancellationToken);
driver.ObservePollOutcome([new DataValueSnapshot(null, statusCode, DateTime.UtcNow, DateTime.UtcNow)]);
driver.GetHealth().LastSuccessfulRead.ShouldBe(before);
}
// ---- ISubscribable (poll-engine overlay) ----
[Fact]