fix(db): classify non-SqlException DB outages as transient; propagate cancellation (#7)
ExecuteWriteAsync only caught SqlException, so a live outage surfacing as InvalidOperationException/SocketException/IOException/TimeoutException escaped unclassified and crashed the script actor instead of buffering. Mirror the HTTP path: propagate OperationCanceledException on cancellation, classify transport exceptions as transient (buffer+retry), let unexpected exceptions propagate.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System.Data.Common;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -62,4 +64,42 @@ public class SqlErrorClassifierTests
|
||||
// be silently retried forever. Unknown => permanent => false.
|
||||
Assert.False(SqlErrorClassifier.IsTransient(errorNumber));
|
||||
}
|
||||
|
||||
// ── M2.3 (#7) code-review fix: IsTransient(Exception) — a live DB outage does
|
||||
// not always surface as a SqlException. Transport/connection/timeout/driver
|
||||
// exception types are transient (buffer+retry), mirroring the HTTP path's
|
||||
// ErrorClassifier.IsTransient(Exception). ──
|
||||
|
||||
public static IEnumerable<object[]> TransientExceptionTypes()
|
||||
{
|
||||
yield return new object[] { new InvalidOperationException("connection not open") };
|
||||
yield return new object[] { new System.IO.IOException("transport reset") };
|
||||
yield return new object[] { new System.Net.Sockets.SocketException(10060) };
|
||||
yield return new object[] { new TimeoutException("timed out") };
|
||||
yield return new object[] { new TaskCanceledException("driver-level cancellation") };
|
||||
// Any DbException that is NOT a SqlException is a driver/transport error.
|
||||
yield return new object[] { new NonSqlDbException("provider transport error") };
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TransientExceptionTypes))]
|
||||
public void IsTransient_Exception_TrueForTransportTypes(Exception ex)
|
||||
{
|
||||
Assert.True(SqlErrorClassifier.IsTransient(ex));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTransient_Exception_FalseForUnexpectedType()
|
||||
{
|
||||
// Authoring bugs are NOT a DB outage — they must propagate, exactly as the
|
||||
// HTTP path lets genuinely-unexpected exceptions escape its IsTransient filter.
|
||||
Assert.False(SqlErrorClassifier.IsTransient(new ArgumentException("authoring bug")));
|
||||
Assert.False(SqlErrorClassifier.IsTransient(new NullReferenceException()));
|
||||
}
|
||||
|
||||
/// <summary>A concrete <see cref="DbException"/> that is not a SqlException, for the classifier unit test.</summary>
|
||||
private sealed class NonSqlDbException : DbException
|
||||
{
|
||||
public NonSqlDbException(string message) : base(message) { }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user