fix(audit): maintenance-login roll-forward/backfill, archive orphan cleanup, head-export health (ScadaBridge#113 #114 #115)
#113: AuditLogPartitionMaintenance (monthly SPLIT RANGE) and the backfill-source-node endpoint now use the AuditLog:MaintenanceConnectionString login when set (unset = unchanged). Migration AuditMaintenanceGrants gives scadabridge_audit_purger ALTER ANY DATASPACE and column-level UPDATE (SourceNode). #114: archive cleanup deletes the writer's own *.tmp files and manifest-less data files older than 24 h inside {Directory}/{YYYY} (DeletedOrphan, dry-run aware). #115: head-export failures bump scadabridge.audit.chain_head_export.failures and Degrade the /health/ready entry audit-chain-head-export (failing or stale > 2 intervals). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+45
-2
@@ -50,7 +50,8 @@ public sealed class AuditChainHeadExportServiceTests
|
||||
new DateTime(2026, month, 1, 0, 0, 0, DateTimeKind.Utc), seq, Enumerable.Repeat((byte)seq, 32).ToArray(),
|
||||
null, null, rows, 0, new DateTime(2026, month, 2, 0, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
private static (AuditChainHeadExportService Service, FakeStore Store, RecordingSink Sink) Create(bool exportUnchanged = false)
|
||||
private static (AuditChainHeadExportService Service, FakeStore Store, RecordingSink Sink) Create(
|
||||
bool exportUnchanged = false, AuditChainHeadExportStatus? status = null, TimeProvider? time = null)
|
||||
{
|
||||
var store = new FakeStore();
|
||||
var sink = new RecordingSink();
|
||||
@@ -62,7 +63,7 @@ public sealed class AuditChainHeadExportServiceTests
|
||||
});
|
||||
var service = new AuditChainHeadExportService(
|
||||
services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>(),
|
||||
options, sink, "central-a", NullLogger<AuditChainHeadExportService>.Instance);
|
||||
options, sink, "central-a", NullLogger<AuditChainHeadExportService>.Instance, time, status);
|
||||
return (service, store, sink);
|
||||
}
|
||||
|
||||
@@ -120,6 +121,48 @@ public sealed class AuditChainHeadExportServiceTests
|
||||
Assert.Equal(10, Assert.Single(Assert.Single(sink.Writes)).LastSeq);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ScadaBridge#115: a failed tick bumps the failure counter metric and the status
|
||||
/// (consecutive + total + last error); the next success clears the consecutive count.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task FailedTick_RecordsMetricAndStatus_SuccessResetsConsecutive()
|
||||
{
|
||||
var status = new AuditChainHeadExportStatus();
|
||||
var (service, store, sink) = Create(status: status);
|
||||
store.Heads.Add(Head(4, 10, 10));
|
||||
long measured = 0;
|
||||
using var listener = new System.Diagnostics.Metrics.MeterListener();
|
||||
listener.InstrumentPublished = (i, l) =>
|
||||
{
|
||||
if (i.Name == "scadabridge.audit.chain_head_export.failures")
|
||||
{
|
||||
l.EnableMeasurementEvents(i);
|
||||
}
|
||||
};
|
||||
listener.SetMeasurementEventCallback<long>((_, v, _, _) => Interlocked.Add(ref measured, v));
|
||||
listener.Start();
|
||||
|
||||
sink.Fail = true;
|
||||
await service.TickAsync(default);
|
||||
await service.TickAsync(default);
|
||||
|
||||
Assert.Equal(2, Interlocked.Read(ref measured));
|
||||
Assert.Equal(2, status.ConsecutiveFailures);
|
||||
Assert.Equal(2, status.TotalFailures);
|
||||
Assert.Equal("share offline", status.LastError);
|
||||
Assert.Null(status.LastSuccessUtc);
|
||||
|
||||
sink.Fail = false;
|
||||
await service.TickAsync(default);
|
||||
|
||||
Assert.Equal(0, status.ConsecutiveFailures);
|
||||
Assert.Equal(2, status.TotalFailures);
|
||||
Assert.NotNull(status.LastSuccessUtc);
|
||||
Assert.Equal(2, Interlocked.Read(ref measured));
|
||||
}
|
||||
|
||||
/// <summary>The validator ignores a disabled export and checks each sink's settings when enabled.</summary>
|
||||
/// <param name="configure">Which settings to apply.</param>
|
||||
/// <param name="expectedError">A fragment of the expected failure, or null when valid.</param>
|
||||
|
||||
Reference in New Issue
Block a user