fix(historian): preserve stable alarm delivery identities
This commit is contained in:
+21
-5
@@ -41,10 +41,14 @@ public sealed class AlarmSfConvergenceTests
|
||||
/// <summary>A writer that never acknowledges — a historian outage, which is when the buffer matters.</summary>
|
||||
private sealed class OutageWriter : IAlarmHistorianWriter
|
||||
{
|
||||
public List<AlarmHistorianEvent> Events { get; } = [];
|
||||
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<IReadOnlyList<HistorianWriteOutcome>>(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken)
|
||||
{
|
||||
Events.AddRange(batch);
|
||||
return Task.FromResult<IReadOnlyList<HistorianWriteOutcome>>(
|
||||
batch.Select(_ => HistorianWriteOutcome.RetryPlease).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
private static AlarmHistorianEvent Event(string alarmId, int second) => new(
|
||||
@@ -170,11 +174,15 @@ public sealed class AlarmSfConvergenceTests
|
||||
await pair.StartAsync();
|
||||
|
||||
var shared = Event("eq/shared", 1);
|
||||
using var sinkA = new LocalDbStoreAndForwardSink(pair.A, new OutageWriter(), Log);
|
||||
using var sinkB = new LocalDbStoreAndForwardSink(pair.B, new OutageWriter(), Log);
|
||||
var sharedOnB = Event("eq/shared", 1);
|
||||
shared.DeliveryId.ShouldNotBe(sharedOnB.DeliveryId);
|
||||
var writerA = new OutageWriter();
|
||||
var writerB = new OutageWriter();
|
||||
using var sinkA = new LocalDbStoreAndForwardSink(pair.A, writerA, Log);
|
||||
using var sinkB = new LocalDbStoreAndForwardSink(pair.B, writerB, Log);
|
||||
|
||||
await sinkA.EnqueueAsync(shared, TestContext.Current.CancellationToken);
|
||||
await sinkB.EnqueueAsync(shared, TestContext.Current.CancellationToken);
|
||||
await sinkB.EnqueueAsync(sharedOnB, TestContext.Current.CancellationToken);
|
||||
|
||||
// A second, DISTINCT event on B. Without it the scenario would pass with replication
|
||||
// switched off entirely — each node would hold its own single copy and "count == 1 on
|
||||
@@ -193,5 +201,13 @@ public sealed class AlarmSfConvergenceTests
|
||||
async () => await CountAsync(pair.A) > 2 || await CountAsync(pair.B) > 2,
|
||||
TimeSpan.FromSeconds(3)))
|
||||
.ShouldBeFalse("the duplicate must stay collapsed");
|
||||
|
||||
// Either node can retry after promotion, with the same GUID despite independent input identities.
|
||||
await sinkA.DrainOnceAsync(TestContext.Current.CancellationToken);
|
||||
await sinkB.DrainOnceAsync(TestContext.Current.CancellationToken);
|
||||
var deliveryA = writerA.Events.Single(e => e.AlarmId == "eq/shared").DeliveryId;
|
||||
var deliveryB = writerB.Events.Single(e => e.AlarmId == "eq/shared").DeliveryId;
|
||||
deliveryA.ShouldNotBe(Guid.Empty);
|
||||
deliveryB.ShouldBe(deliveryA);
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -36,6 +36,38 @@ public sealed class AlarmSfLegacyMigratorTests : IDisposable
|
||||
|
||||
private ServiceProvider? _provider;
|
||||
|
||||
private sealed class CapturingRetryWriter : IAlarmHistorianWriter
|
||||
{
|
||||
public List<AlarmHistorianEvent> Events { get; } = [];
|
||||
public Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken)
|
||||
{
|
||||
Events.AddRange(batch);
|
||||
return Task.FromResult<IReadOnlyList<HistorianWriteOutcome>>(batch.Select(_ => HistorianWriteOutcome.RetryPlease).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_legacy_payload_keeps_its_key_and_stable_delivery_identity_across_drains()
|
||||
{
|
||||
var payload = Payload("eq/a");
|
||||
SeedLegacy((1, "eq/a", payload, 0, dead: 0));
|
||||
var db = BuildDb();
|
||||
AlarmSfLegacyMigrator.Migrate(db, ConfigWithLegacyPath());
|
||||
var row = (await db.QueryAsync("SELECT id, payload_json FROM alarm_sf_events",
|
||||
r => (Id: r.GetString(0), Payload: r.GetString(1)), null, TestContext.Current.CancellationToken)).Single();
|
||||
row.Id.ShouldBe("B1D80B7207C3F3B6CB31105DBF1A6E0D6FE300DA1B3F877AB746B36535310A60");
|
||||
row.Payload.ShouldBe(payload);
|
||||
var writer = new CapturingRetryWriter();
|
||||
using var log = new Serilog.LoggerConfiguration().CreateLogger();
|
||||
using (var sink = new LocalDbStoreAndForwardSink(db, writer, log))
|
||||
await sink.DrainOnceAsync(TestContext.Current.CancellationToken);
|
||||
using (var sink = new LocalDbStoreAndForwardSink(db, writer, log))
|
||||
await sink.DrainOnceAsync(TestContext.Current.CancellationToken);
|
||||
writer.Events.Count.ShouldBe(2);
|
||||
writer.Events.ShouldAllBe(evt => evt.DeliveryId == Guid.Parse("55372883-eaec-8dd7-a1a4-6f98f416a272"));
|
||||
}
|
||||
|
||||
// ---- the cases ---------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user