fix(config-db): EnableRetryOnFailure fleet-wide + execution-strategy wrap of the combined-telemetry dual-write

Installs SqlServerRetryingExecutionStrategy (5 retries, 30s cap) so transient SQL
faults retry transparently on every read-side path. Manual transactions aren't
auto-retried, so AuditLogIngestActor's idempotent dual-write is wrapped in an
explicit CreateExecutionStrategy().ExecuteAsync to become retriable. Verified
empirically that existing manual transactions (BundleImporter) are unaffected —
EF skips the strategy while a transaction is already active.
This commit is contained in:
Joseph Doherty
2026-07-09 08:03:30 -04:00
parent 6b06d1efcb
commit 1dd89d523b
4 changed files with 90 additions and 26 deletions
@@ -1,4 +1,5 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
@@ -13,7 +14,7 @@ public class ServiceCollectionExtensionsTests
{
var services = new ServiceCollection();
services.AddConfigurationDatabase("DataSource=:memory:");
services.AddConfigurationDatabase("Server=unused;Database=unused;");
Assert.Contains(services, d => d.ServiceType == typeof(ITemplateEngineRepository));
Assert.Contains(services, d => d.ServiceType == typeof(IAuditService));
@@ -21,6 +22,32 @@ public class ServiceCollectionExtensionsTests
Assert.Contains(services, d => d.ServiceType == typeof(IAuditLogRepository));
}
[Fact]
public void AddConfigurationDatabase_Configures_RetryingExecutionStrategy()
{
// Task 12 (arch-review 04 S5): the central DbContext must survive transient
// SQL faults. EnableRetryOnFailure installs a non-null ExecutionStrategyFactory
// on the SqlServer options extension — covering every read-side path (KPI asks,
// outbox queries, Tracking.Status, execution trees) that previously surfaced
// transient faults raw.
var services = new ServiceCollection();
services.AddConfigurationDatabase("Server=unused;Database=unused;");
using var sp = services.BuildServiceProvider();
var options = sp.GetRequiredService<DbContextOptions<ScadaBridgeDbContext>>();
// EF1001: SqlServerOptionsExtension is an internal EF surface, but it is the
// only place the retrying-strategy factory is observable — asserting it is the
// point of this test. Scoped suppression, not a project-wide one.
#pragma warning disable EF1001
var sqlExt = options.Extensions
.OfType<Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal.SqlServerOptionsExtension>()
.Single();
Assert.NotNull(sqlExt.ExecutionStrategyFactory);
#pragma warning restore EF1001
}
// The no-arg overload is [Obsolete(error: true)], so it cannot be referenced directly
// from source — that is the compile-time guard. Invoke it via reflection to verify the
// runtime defence-in-depth behaviour.