fix(driver.sql): defer HandlePollError on the exception ReadAsync classified, not on its type (Driver.Sql-004)
This commit is contained in:
@@ -155,6 +155,20 @@ public sealed class SqlDriver
|
||||
|
||||
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// The exception instance <see cref="ReadAsync"/> has already turned into a health verdict, held only
|
||||
/// until <see cref="HandlePollError"/> — which the poll engine calls with that same object — has
|
||||
/// consumed it (Driver.Sql-004).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A field rather than a flag on the exception itself: <see cref="Exception.Data"/> is provider-owned
|
||||
/// and may be read-only, and wrapping the throw would change the type the engine and
|
||||
/// <see cref="IReadable"/>'s callers see. Concurrent poll loops can overwrite each other's mark; the
|
||||
/// loser is then merely re-degraded with a poorer message, which is the pre-fix behaviour rather than
|
||||
/// a new failure.
|
||||
/// </remarks>
|
||||
private Exception? _classifiedByRead;
|
||||
|
||||
/// <summary>Occurs when a subscribed tag's value or quality changes.</summary>
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
|
||||
@@ -528,6 +542,10 @@ public sealed class SqlDriver
|
||||
// on the log sink via the LogWarning exception parameter above.
|
||||
Degrade($"Sql driver '{_driverInstanceId}' could not reach {Endpoint}: {ex.GetType().Name}.");
|
||||
TransitionHostTo(HostState.Stopped);
|
||||
// Mark THIS exception object as already classified before it goes back to the poll engine, which
|
||||
// routes it to HandlePollError next. The mark is what makes the deferral there exact — see that
|
||||
// method for why inferring it from the exception's type was not (Driver.Sql-004).
|
||||
Volatile.Write(ref _classifiedByRead, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -650,11 +668,20 @@ public sealed class SqlDriver
|
||||
/// <param name="ex">The exception the poll engine caught.</param>
|
||||
internal void HandlePollError(Exception ex)
|
||||
{
|
||||
// A subscribed-read outage surfaces as a DbException the reader threw, which ReadAsync has ALREADY
|
||||
// classified — good, endpoint-bearing LastError + host Stopped — before rethrowing. The poll engine
|
||||
// then routes that same exception here. Re-degrading would overwrite the good LastError with a worse,
|
||||
// guidance-free one and log the outage a second time, so skip the connection class ReadAsync owns.
|
||||
if (ex is DbException) return;
|
||||
// A subscribed-read outage is classified by ReadAsync — good, endpoint-bearing LastError + host
|
||||
// Stopped — before it rethrows, and the poll engine then routes that SAME exception object here.
|
||||
// Re-degrading would overwrite the good LastError with a worse, guidance-free one and log the outage
|
||||
// a second time, so defer to the classification that already happened.
|
||||
//
|
||||
// Matched on the exception INSTANCE ReadAsync marked, not on its type. `ex is DbException` was the
|
||||
// stated intent — "the class ReadAsync owns" — but not what it implemented (Driver.Sql-004): the
|
||||
// reader raises a plain InvalidOperationException when the provider factory returns no connection,
|
||||
// and a provider can raise ArgumentException/FormatException off a malformed connection string on
|
||||
// the same open path. Those are classified by ReadAsync exactly like a DbException, yet a type test
|
||||
// let them through and clobbered its message; conversely a DbException that never went through
|
||||
// ReadAsync — the poll engine's own throw — was swallowed. Consumed once, so a later unclassified
|
||||
// exception is never mistaken for this one.
|
||||
if (ReferenceEquals(Interlocked.CompareExchange(ref _classifiedByRead, null, ex), ex)) return;
|
||||
|
||||
// Whatever reaches here — an engine contract-violation, or a non-DbException the provider raised
|
||||
// (e.g. an ArgumentException from a malformed connection string, which the parser echoes with
|
||||
|
||||
Reference in New Issue
Block a user