feat(localdb): alarm_sf_events replicated table
Adds the alarm store-and-forward buffer to the consolidated LocalDb file and registers it for replication, so a node that dies with undelivered alarm history no longer takes it to the grave. The table keeps the legacy queue's shape rather than the status column the plan sketched. An acknowledged row is deleted, not marked delivered: that needs no second sweeper to keep the table bounded, leaves the capacity semantics untouched, and the replication engine carries the delete as a tombstone so the peer drops its copy anyway -- which is what the status column was for. last_error is retained because it is the only operator-facing record of why a row was dead-lettered. The primary key is app-minted TEXT. The legacy AUTOINCREMENT RowId cannot replicate under last-writer-wins: two nodes would independently allocate rowid 7 to different alarms and silently overwrite each other. The drain index therefore orders by enqueued_at_utc rather than insertion order, with id as a tiebreak so the ordering is total. Tables are created unconditionally, independent of AlarmHistorian:Enabled. An empty registered table costs three triggers; creating it lazily would mean a node that enables the historian later writes rows before its capture triggers exist, which is exactly the silent-loss shape OnReady's ordering comment warns about. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -46,7 +46,7 @@ public sealed class LocalDbSetupTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnReady_RegistersExactlyTheTwoDeploymentTables()
|
||||
public void OnReady_RegistersExactlyTheThreeReplicatedTables()
|
||||
{
|
||||
// Both directions are load-bearing. "No fewer" catches a dropped RegisterReplicated call
|
||||
// (that table then never replicates). "No more" catches an accidental registration —
|
||||
@@ -54,7 +54,7 @@ public sealed class LocalDbSetupTests : IDisposable
|
||||
var db = BuildDb();
|
||||
|
||||
db.ReplicatedTables.Keys.OrderBy(k => k, StringComparer.Ordinal)
|
||||
.ShouldBe(["deployment_artifacts", "deployment_pointer"]);
|
||||
.ShouldBe(["alarm_sf_events", "deployment_artifacts", "deployment_pointer"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -78,6 +78,50 @@ public sealed class LocalDbSetupTests : IDisposable
|
||||
db.ReplicatedTables["deployment_pointer"].PkColumns.ShouldBe(["cluster_id"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlarmSfEvents_PkIsTheAppMintedId()
|
||||
{
|
||||
// A single TEXT PK the application mints. The legacy queue this table replaces keyed on
|
||||
// `RowId INTEGER PRIMARY KEY AUTOINCREMENT`, which LWW cannot replicate: two nodes would
|
||||
// independently allocate rowid 7 for different alarms and silently overwrite each other.
|
||||
var db = BuildDb();
|
||||
|
||||
db.ReplicatedTables["alarm_sf_events"].PkColumns.ShouldBe(["id"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlarmSfEventRows_EnterTheOplog()
|
||||
{
|
||||
// Same ordering assertion as the deployment-pointer test below, for the table Phase 2 adds.
|
||||
// Worth its own case because the alarm table is registered by a different call site, and a
|
||||
// registration added ahead of its DDL would fail loudly while one added after the migrator
|
||||
// would fail silently.
|
||||
var db = BuildDb();
|
||||
|
||||
await db.ExecuteAsync(
|
||||
"""
|
||||
INSERT INTO alarm_sf_events
|
||||
(id, alarm_id, enqueued_at_utc, payload_json, attempt_count)
|
||||
VALUES (@Id, @AlarmId, @EnqueuedAtUtc, @PayloadJson, 0)
|
||||
""",
|
||||
new
|
||||
{
|
||||
Id = new string('c', 64),
|
||||
AlarmId = "equip/alarm-1",
|
||||
EnqueuedAtUtc = "2026-07-21T00:00:00.0000000Z",
|
||||
PayloadJson = """{"AlarmId":"equip/alarm-1"}""",
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
var oplogRows = await db.QueryAsync(
|
||||
"SELECT COUNT(*) FROM __localdb_oplog WHERE table_name = 'alarm_sf_events'",
|
||||
r => r.GetInt32(0),
|
||||
parameters: null,
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
oplogRows[0].ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RowsWrittenAfterOnReady_EnterTheOplog()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user