fix(driver.sql): a post-open query failure is not a reachability verdict (Driver.Sql-005)
This commit is contained in:
@@ -597,9 +597,11 @@ public sealed class SqlDriver
|
||||
/// <list type="bullet">
|
||||
/// <item>Any Good snapshot ⇒ the database answered: <c>LastSuccessfulRead</c> advances and the
|
||||
/// host is Running.</item>
|
||||
/// <item>Any <b>connection-class</b> Bad snapshot (<see cref="SqlStatusCodes.BadTimeout"/> /
|
||||
/// <see cref="SqlStatusCodes.BadCommunicationError"/>) ⇒ Degraded; when nothing at all was Good,
|
||||
/// the host is Stopped.</item>
|
||||
/// <item>Any <b>connection-class</b> Bad snapshot (<see cref="SqlStatusCodes.BadTimeout"/>) ⇒
|
||||
/// Degraded; when nothing at all answered, the host is Stopped.</item>
|
||||
/// <item>Any <b>query-failure</b> Bad snapshot (<see cref="SqlStatusCodes.BadCommunicationError"/>)
|
||||
/// ⇒ Degraded, but the host stays Running. See <see cref="IsQueryFailureClass"/>: that code is
|
||||
/// minted only after the connection opened, so it is evidence the endpoint is up.</item>
|
||||
/// <item>Bad codes that are <b>authoring</b> facts — an unresolvable RawPath, an absent row, a
|
||||
/// type mismatch, a rejected definition — change nothing. A tag typo must never report the
|
||||
/// database as down and send an operator to the wrong system.</item>
|
||||
@@ -620,18 +622,19 @@ public sealed class SqlDriver
|
||||
|
||||
var good = 0;
|
||||
var unreachable = 0;
|
||||
var queryFailed = 0;
|
||||
foreach (var snapshot in snapshots)
|
||||
{
|
||||
if (SqlStatusCodes.IsGood(snapshot.StatusCode)) good++;
|
||||
else if (IsConnectionClass(snapshot.StatusCode)) unreachable++;
|
||||
else if (IsQueryFailureClass(snapshot.StatusCode)) queryFailed++;
|
||||
}
|
||||
|
||||
if (unreachable > 0)
|
||||
{
|
||||
Degrade(
|
||||
$"{unreachable} of {snapshots.Count} Sql tag(s) timed out or failed communication against " +
|
||||
$"{Endpoint} on the last poll.");
|
||||
if (good > 0)
|
||||
$"{unreachable} of {snapshots.Count} Sql tag(s) timed out against {Endpoint} on the last poll.");
|
||||
if (good > 0 || queryFailed > 0)
|
||||
{
|
||||
// Partial: something answered, so the endpoint is up — but the driver is not healthy.
|
||||
TouchLastSuccessfulRead();
|
||||
@@ -645,6 +648,21 @@ public sealed class SqlDriver
|
||||
return;
|
||||
}
|
||||
|
||||
if (queryFailed > 0)
|
||||
{
|
||||
// The connection opened and the server answered — with an error. Degraded (every tag in that
|
||||
// group is Bad) but emphatically NOT a reachability verdict: reporting the endpoint down over a
|
||||
// dropped table or a revoked SELECT points the operator at the wrong system, and the reconnect
|
||||
// it would eventually provoke cannot fix a query.
|
||||
Degrade(
|
||||
$"{queryFailed} of {snapshots.Count} Sql tag(s) failed their query against {Endpoint} on the " +
|
||||
"last poll; the connection opened, so check the table/column, the grant and any lock rather " +
|
||||
"than the network.");
|
||||
TouchLastSuccessfulRead();
|
||||
TransitionHostTo(HostState.Running);
|
||||
return;
|
||||
}
|
||||
|
||||
if (good == 0) return; // authoring-class Bad only — not a statement about the database.
|
||||
|
||||
// Route through the Faulted guard, not a raw WriteHealth: a poll still in flight when a concurrent
|
||||
@@ -655,9 +673,28 @@ public sealed class SqlDriver
|
||||
TransitionHostTo(HostState.Running);
|
||||
}
|
||||
|
||||
/// <summary>True for the status codes that mean "the database did not answer", as opposed to "the data is not there".</summary>
|
||||
private static bool IsConnectionClass(uint statusCode)
|
||||
=> statusCode is SqlStatusCodes.BadTimeout or SqlStatusCodes.BadCommunicationError;
|
||||
/// <summary>
|
||||
/// True for the status codes that mean "the database did not answer", as opposed to "the data is not
|
||||
/// there" or "the query was refused".
|
||||
/// <para><see cref="SqlStatusCodes.BadCommunicationError"/> is deliberately <b>not</b> here despite
|
||||
/// its name (Driver.Sql-005): the reader mints it from <c>GroupResult.Failed</c>, which is reachable
|
||||
/// only after <c>OpenAsync</c> returned. Treating it as "did not answer" made one group failing on a
|
||||
/// schema change, a lock or a revoked grant report the whole endpoint down. See
|
||||
/// <see cref="IsQueryFailureClass"/>.</para>
|
||||
/// </summary>
|
||||
/// <param name="statusCode">The status code to classify.</param>
|
||||
/// <returns>True when the code means the database did not answer at all.</returns>
|
||||
private static bool IsConnectionClass(uint statusCode) => statusCode is SqlStatusCodes.BadTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// True for the status codes that mean "the connection opened but the query failed" — a schema
|
||||
/// change, a lock, a missing grant, a statement the provider rejected. Degrading, but proof the
|
||||
/// endpoint is up.
|
||||
/// </summary>
|
||||
/// <param name="statusCode">The status code to classify.</param>
|
||||
/// <returns>True when the code means the server answered the connection and refused the query.</returns>
|
||||
private static bool IsQueryFailureClass(uint statusCode)
|
||||
=> statusCode is SqlStatusCodes.BadCommunicationError;
|
||||
|
||||
/// <summary>
|
||||
/// Routes a poll-loop reader failure to the health surface (05/STAB-9). Deliberately does <b>not</b>
|
||||
|
||||
@@ -224,6 +224,33 @@ public sealed class SqlDriverTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_whenTheQueryFailsAfterTheConnectionOpened_degradesButLeavesTheEndpointUp()
|
||||
{
|
||||
// Driver.Sql-005. BadCommunicationError is minted by GroupResult.Failed, which the reader raises only
|
||||
// AFTER the connection opened ("the connection opened, so the server is up — this query is what
|
||||
// failed"). Classifying it as connection-class reported the whole database endpoint Stopped over one
|
||||
// group failing on a dropped table, a revoked SELECT or an over-long IN list, sending the operator at
|
||||
// the network instead of at the schema. The driver must still degrade — every tag in that group is
|
||||
// Bad — but the endpoint is demonstrably up.
|
||||
using var fixture = new SqlitePollFixture();
|
||||
await using var driver = NewDriver(fixture, KvEntry("Speed", SqlitePollFixture.PresentKey));
|
||||
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
|
||||
driver.GetHostStatuses()[0].State.ShouldBe(HostState.Running);
|
||||
|
||||
// The schema drifts out from under a driver whose catalog gate already passed.
|
||||
fixture.Execute($"DROP TABLE {SqlitePollFixture.KeyValueTable}");
|
||||
|
||||
var snapshots = await driver.ReadAsync(["Speed"], CancellationToken.None);
|
||||
|
||||
snapshots[0].StatusCode.ShouldBe(SqlStatusCodes.BadCommunicationError);
|
||||
var health = driver.GetHealth();
|
||||
health.State.ShouldBe(DriverState.Degraded);
|
||||
health.LastError.ShouldNotBeNull();
|
||||
health.LastError.ShouldContain("quer"); // names the query, not the network
|
||||
driver.GetHostStatuses()[0].State.ShouldBe(HostState.Running); // RED pre-fix: Stopped
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_anUnresolvableRef_isNotAConnectivityFact_soHealthAndHostStand()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user