af545efdf5
Copies the pre-consolidation store-and-forward queue into the consolidated
database on first boot, then renames the legacy file aside. Rows are in that
file precisely because the historian could not be reached, so dropping them
on upgrade would discard exactly the alarm audit trail the queue exists to
protect.
Runs last in OnReady, after every RegisterReplicated call. It is the only
thing in OnReady that writes rows, and capture is trigger-based: a migration
that ran before registration would recover the backlog locally and never
replicate a line of it, silently and permanently.
Ids are derived from the payload rather than the plan's mig-{node}-{legacyId}
scheme. Node-prefixing solves the collision the legacy AUTOINCREMENT key
would cause -- node A's row 7 and node B's row 7 are different alarms -- but
it preserves a duplication that should be collapsed instead. A warm pair's
two legacy files OVERLAP: HistorianAdapterActor default-writes while its
redundancy role is unknown, so both nodes accepted the same transitions
during every boot window. Prefixed ids would carry those duplicates into the
merged buffer forever; equal-payload ids converge them. The same property
makes a crash between commit and rename harmless under INSERT OR IGNORE.
OnReady now takes IConfiguration rather than offering an overload that skips
the migration. A wiring mistake that silently discarded a node's undelivered
alarm history is not a mistake worth making possible.
The copy is restricted to the columns the legacy table actually has. Naming
a column an older build never wrote throws "no such column", which would
discard every row in the table rather than the one field.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
83 lines
3.6 KiB
JSON
83 lines
3.6 KiB
JSON
{
|
|
"planPath": "docs/plans/2026-07-20-localdb-adoption-phase2.md",
|
|
"tasks": [
|
|
{
|
|
"id": 0,
|
|
"subject": "Task 0: Recon \u2014 sink schema, seam, drain lifecycle, role-view bridge (STOP conditions)",
|
|
"status": "completed",
|
|
"note": "STOP condition does NOT fire (no BLOB; PayloadJson TEXT). Recon doc: docs/plans/2026-07-20-localdb-phase2-recon.md. Key finding: the drain worker is an internal Timer inside the sink (not a hosted service/actor), so the gate is a Func<bool> ctor param; and HistorianAdapterActor already primary-gates ENQUEUE with a different policy (ShouldHistorize) - which is why today's ungated drain is safe and why replication breaks it. Deviations D-1..D-4 recorded."
|
|
},
|
|
{
|
|
"id": 1,
|
|
"subject": "Task 1: alarm_sf_events schema + registration (+ exact-set pin update)",
|
|
"status": "completed",
|
|
"blockedBy": [
|
|
0
|
|
],
|
|
"note": "alarm_sf_events created in AlarmSfSchema (Core.AlarmHistorian) + registered third in LocalDbSetup.OnReady. Exact-set pin updated to 3 tables. DEVIATION D-5: kept the legacy delete-on-ack + dead_lettered flag + last_error rather than the plan's status column (no sweeper for 'delivered' rows; last_error is the only record of why a row died). Drain ORDER BY moves to (enqueued_at_utc, id)."
|
|
},
|
|
{
|
|
"id": 2,
|
|
"subject": "Task 2: Rewire sink onto ILocalDb + delete bespoke file management (cutover 1/2)",
|
|
"status": "completed",
|
|
"blockedBy": [
|
|
1
|
|
],
|
|
"note": "Sink rewritten as LocalDbStoreAndForwardSink over ILocalDb; bespoke file/pragma/schema management deleted with the old class. AlarmHistorian:DatabasePath removed (breaking config key). Ids are a deterministic payload hash (D-1), not GUIDs."
|
|
},
|
|
{
|
|
"id": 3,
|
|
"subject": "Task 3: Primary-gated drain via PrimaryGatePolicy (cutover 2/2 \u2014 may co-commit with Task 2)",
|
|
"status": "completed",
|
|
"blockedBy": [
|
|
2
|
|
],
|
|
"note": "Drain gated on IRedundancyRoleView, a singleton DriverHostActor publishes PrimaryGatePolicy's verdict to on every snapshot. Fails closed on a throwing gate; seeded OPEN so a non-redundant deployment is never silently stopped. New HistorianDrainState.NotPrimary + transition-logged (D-2). Landed with Task 2 as one commit (D-3)."
|
|
},
|
|
{
|
|
"id": 4,
|
|
"subject": "Task 4: One-time alarm-historian.db legacy migrator",
|
|
"status": "completed",
|
|
"blockedBy": [
|
|
3
|
|
],
|
|
"note": "AlarmSfLegacyMigrator runs LAST in OnReady (which now takes IConfiguration - no skip-migration overload exists). DEVIATION D-6: ids are the payload hash (AlarmSfSchema.DeriveId, lifted out of the sink) rather than mig-{node}-{legacyId} - a warm pair's two legacy files OVERLAP, and node-prefixing would carry that duplication forward forever. DEVIATION: tests live in Host.IntegrationTests; the plan's Host.Tests project does not exist."
|
|
},
|
|
{
|
|
"id": 5,
|
|
"subject": "Task 5: Convergence + failover scenarios in the pair harness (+ positive control)",
|
|
"status": "pending",
|
|
"blockedBy": [
|
|
3
|
|
]
|
|
},
|
|
{
|
|
"id": 6,
|
|
"subject": "Task 6: Rig config + docs",
|
|
"status": "pending",
|
|
"blockedBy": [
|
|
3
|
|
]
|
|
},
|
|
{
|
|
"id": 7,
|
|
"subject": "Task 7: DoD sweep (offline) \u2014 STOP and report after this",
|
|
"status": "pending",
|
|
"blockedBy": [
|
|
4,
|
|
5,
|
|
6
|
|
]
|
|
},
|
|
{
|
|
"id": 8,
|
|
"subject": "Task 8: Live gate on the docker-dev rig (needs explicit user go-ahead)",
|
|
"status": "pending",
|
|
"blockedBy": [
|
|
7
|
|
]
|
|
}
|
|
],
|
|
"lastUpdated": "2026-07-21T00:00:00Z"
|
|
}
|