fix(kpi): classify the failover fold-race pass loss as a self-healing Information event (plan R2-04 T9)

This commit is contained in:
Joseph Doherty
2026-07-13 10:21:02 -04:00
parent cd93ad3976
commit 439dee52d3
3 changed files with 128 additions and 3 deletions
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
@@ -13,14 +15,22 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
public sealed class KpiHistoryRepository : IKpiHistoryRepository
{
private readonly ScadaBridgeDbContext _context;
private readonly ILogger<KpiHistoryRepository> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="KpiHistoryRepository"/> class.
/// </summary>
/// <param name="context">The EF Core database context.</param>
public KpiHistoryRepository(ScadaBridgeDbContext context)
/// <param name="logger">
/// Optional logger; defaults to <see cref="NullLogger{T}"/> (mirrors
/// <c>SiteCallAuditRepository</c> — MS.DI resolves <see cref="ILogger{T}"/> automatically,
/// so no registration churn). Used to classify the self-healing failover fold-race.
/// </param>
public KpiHistoryRepository(
ScadaBridgeDbContext context, ILogger<KpiHistoryRepository>? logger = null)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? NullLogger<KpiHistoryRepository>.Instance;
}
/// <inheritdoc />
@@ -170,7 +180,22 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository
}
}
await _context.SaveChangesAsync(cancellationToken);
try
{
await _context.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException ex)
{
// Failover-overlap race: the old singleton incarnation's in-flight fold and the
// new node's first fold can both Add the same (series, hour) row; the unique
// IX_KpiRollupHourly_Series then faults the loser's ENTIRE SaveChanges — the
// failure grain is the PASS, not the row (arch-review 04 round 2, R5). The fold
// only ever writes KpiRollupHourly rows, so any save fault here is a lost upsert
// race or a transient the next idempotent re-fold repairs identically; classify
// at Information instead of surfacing a scary error for a self-healing no-op.
_logger.LogInformation(ex,
"KPI rollup fold lost a failover-overlap upsert race; pass discarded, next fold self-heals.");
}
}
/// <inheritdoc />