Phase 7 Stream D — Historian alarm sink (SQLite store-and-forward + Galaxy.Host IPC contracts) #182

Merged
dohertj2 merged 1 commits from phase-7-stream-d-alarm-historian into v2 2026-04-20 19:14:03 -04:00
Owner

Ships Phase 7 plan decisions #16, #17, #19, #21: durable local SQLite queue absorbs every qualifying alarm event, a drain worker forwards batches to Galaxy.Host (which reuses the already-loaded 32-bit aahClientManaged DLLs) on an exponential-backoff cadence, and operator acks never block on the historian being reachable.

New project Core.AlarmHistorian (net10)

  • AlarmHistorianEvent — source-agnostic event shape (scripted alarms + Galaxy-native + AB CIP ALMD + any future IAlarmSource per plan decision #15)
  • IAlarmHistorianSink + NullAlarmHistorianSink — interface + disabled default
  • IAlarmHistorianWriter — per-event outcome (Ack / RetryPlease / PermanentFail); Stream G wires the Galaxy.Host IPC client implementation
  • SqliteStoreAndForwardSink — full implementation:
    • Queue table with AttemptCount / LastError / DeadLettered columns + index
    • DrainOnceAsync serialised via SemaphoreSlim
    • BackoffLadder 1s → 2s → 5s → 15s → 60s (cap)
    • DefaultCapacity = 1_000_000 rows — overflow evicts oldest non-dead-lettered
    • DefaultDeadLetterRetention = 30 days — sweeper purges on every drain tick
    • RetryDeadLettered operator action reattaches dead-letters to the regular queue
    • Writer-side exceptions treated as whole-batch RetryPlease (no data loss)

New IPC contracts in Driver.Galaxy.Shared

  • HistorianAlarmEventRequest — batched up to 100 events/request per Phase 7 plan Stream D.5
  • HistorianAlarmEventResponse — per-event outcome (1:1 with request order) so a single malformed event can be dead-lettered without blocking neighbors
  • HistorianAlarmEventOutcomeDto : byteAck / RetryPlease / PermanentFail
  • HistorianAlarmEventDto — mirrors Core.AlarmHistorian.AlarmHistorianEvent
  • HistorianConnectivityStatusNotification — Host pushes proactively when the SDK session drops so /alarms/historian flips red without waiting for the next drain cycle
  • MessageKind additions: 0x80 HistorianAlarmEventRequest / 0x81 HistorianAlarmEventResponse / 0x82 HistorianConnectivityStatus

Tests — 14/14

SqliteStoreAndForwardSinkTests covers:

  • enqueue → drain → Ack round-trip (removes row + sets LastSuccessUtc)
  • empty-queue drain is no-op (no writer call, Idle state)
  • RetryPlease bumps backoff + keeps row + flips state to BackingOff
  • Ack after Retry resets backoff to 1s
  • PermanentFail dead-letters one row — neighbor in batch Ack's normally
  • Writer-side exception treated as whole-batch RetryPlease with LastError surfaced
  • Capacity eviction drops oldest non-dead-lettered row
  • Dead-lettered rows purged past retention window (injectable clock)
  • RetryDeadLettered operator action requeues all dead-letters + resets AttemptCount
  • BackoffLadder caps at 60s after 10 consecutive retries
  • NullAlarmHistorianSink reports Disabled status + swallows enqueue
  • Constructor argument validation
  • Disposed sink rejects subsequent Enqueue

Totals

Full Phase 7 tests: 160 green (63 Scripting + 36 VirtualTags + 47 ScriptedAlarms + 14 AlarmHistorian). Stream G wires the IPC contracts into the real Galaxy.Host pipe handler.

Ships Phase 7 plan decisions #16, #17, #19, #21: durable local SQLite queue absorbs every qualifying alarm event, a drain worker forwards batches to Galaxy.Host (which reuses the already-loaded 32-bit `aahClientManaged` DLLs) on an exponential-backoff cadence, and operator acks never block on the historian being reachable. ## New project `Core.AlarmHistorian` (net10) - **`AlarmHistorianEvent`** — source-agnostic event shape (scripted alarms + Galaxy-native + AB CIP ALMD + any future IAlarmSource per plan decision #15) - **`IAlarmHistorianSink`** + **`NullAlarmHistorianSink`** — interface + disabled default - **`IAlarmHistorianWriter`** — per-event outcome (Ack / RetryPlease / PermanentFail); Stream G wires the Galaxy.Host IPC client implementation - **`SqliteStoreAndForwardSink`** — full implementation: - Queue table with `AttemptCount` / `LastError` / `DeadLettered` columns + index - `DrainOnceAsync` serialised via `SemaphoreSlim` - BackoffLadder `1s → 2s → 5s → 15s → 60s` (cap) - `DefaultCapacity = 1_000_000` rows — overflow evicts oldest non-dead-lettered - `DefaultDeadLetterRetention = 30 days` — sweeper purges on every drain tick - `RetryDeadLettered` operator action reattaches dead-letters to the regular queue - Writer-side exceptions treated as whole-batch RetryPlease (no data loss) ## New IPC contracts in `Driver.Galaxy.Shared` - `HistorianAlarmEventRequest` — batched up to 100 events/request per Phase 7 plan Stream D.5 - `HistorianAlarmEventResponse` — per-event outcome (1:1 with request order) so a single malformed event can be dead-lettered without blocking neighbors - `HistorianAlarmEventOutcomeDto : byte` — `Ack` / `RetryPlease` / `PermanentFail` - `HistorianAlarmEventDto` — mirrors `Core.AlarmHistorian.AlarmHistorianEvent` - `HistorianConnectivityStatusNotification` — Host pushes proactively when the SDK session drops so `/alarms/historian` flips red without waiting for the next drain cycle - `MessageKind` additions: `0x80` `HistorianAlarmEventRequest` / `0x81` `HistorianAlarmEventResponse` / `0x82` `HistorianConnectivityStatus` ## Tests — 14/14 `SqliteStoreAndForwardSinkTests` covers: - enqueue → drain → Ack round-trip (removes row + sets LastSuccessUtc) - empty-queue drain is no-op (no writer call, Idle state) - RetryPlease bumps backoff + keeps row + flips state to BackingOff - Ack after Retry resets backoff to 1s - PermanentFail dead-letters one row — neighbor in batch Ack's normally - Writer-side exception treated as whole-batch RetryPlease with LastError surfaced - Capacity eviction drops oldest non-dead-lettered row - Dead-lettered rows purged past retention window (injectable clock) - RetryDeadLettered operator action requeues all dead-letters + resets AttemptCount - BackoffLadder caps at 60s after 10 consecutive retries - NullAlarmHistorianSink reports Disabled status + swallows enqueue - Constructor argument validation - Disposed sink rejects subsequent Enqueue ## Totals Full Phase 7 tests: **160 green** (63 Scripting + 36 VirtualTags + 47 ScriptedAlarms + 14 AlarmHistorian). Stream G wires the IPC contracts into the real Galaxy.Host pipe handler.
dohertj2 added 1 commit 2026-04-20 19:13:32 -04:00
Phase 7 plan decisions #16, #17, #19, #21 implementation. Durable local SQLite queue
absorbs every qualifying alarm event; drain worker forwards batches to Galaxy.Host
(reusing the already-loaded 32-bit aahClientManaged DLLs) on an exponential-backoff
cadence; operator acks never block on the historian being reachable.

## New project Core.AlarmHistorian (net10)

- AlarmHistorianEvent — source-agnostic event shape (scripted alarms + Galaxy-native +
  AB CIP ALMD + any future IAlarmSource)
- IAlarmHistorianSink / NullAlarmHistorianSink — interface + disabled default
- IAlarmHistorianWriter — per-event outcome (Ack / RetryPlease / PermanentFail); Stream G
  wires the Galaxy.Host IPC client implementation
- SqliteStoreAndForwardSink — full implementation:
  - Queue table with AttemptCount / LastError / DeadLettered columns
  - DrainOnceAsync serialised via SemaphoreSlim
  - BackoffLadder 1s → 2s → 5s → 15s → 60s (cap)
  - DefaultCapacity 1,000,000 rows — overflow evicts oldest non-dead-lettered
  - DefaultDeadLetterRetention 30 days — sweeper purges on every drain tick
  - RetryDeadLettered operator action reattaches dead-letters to the regular queue
  - Writer-side exceptions treated as whole-batch RetryPlease (no data loss)

## New IPC contracts in Driver.Galaxy.Shared

- HistorianAlarmEventRequest — batched up to 100 events/request per plan Stream D.5
- HistorianAlarmEventResponse — per-event outcome (1:1 with request order)
- HistorianAlarmEventOutcomeDto enum (byte on the wire — Ack/RetryPlease/PermanentFail)
- HistorianAlarmEventDto — mirrors Core.AlarmHistorian.AlarmHistorianEvent
- HistorianConnectivityStatusNotification — Host pushes proactively when the SDK
  session drops so /alarms/historian flips red without waiting for the next drain
- MessageKind additions: 0x80 HistorianAlarmEventRequest / 0x81 HistorianAlarmEventResponse
  / 0x82 HistorianConnectivityStatus

## Tests — 14/14

SqliteStoreAndForwardSinkTests covers: enqueue→drain→Ack round-trip, empty-queue no-op,
RetryPlease bumps backoff + keeps row, Ack after Retry resets backoff, PermanentFail
dead-letters one row without blocking neighbors, writer exception treated as whole-batch
retry with error surfaced in status, capacity eviction drops oldest non-dead-lettered,
dead-letters purged past retention window, RetryDeadLettered requeues, ladder caps at
60s after 10 retries, Null sink reports Disabled status, null sink swallows enqueue,
ctor argument validation, disposed sink rejects enqueue.

## Totals
Full Phase 7 tests: 160 green (63 Scripting + 36 VirtualTags + 47 ScriptedAlarms +
14 AlarmHistorian). Stream G wires this into the real Galaxy.Host IPC pipe.
dohertj2 merged commit dccaa11510 into v2 2026-04-20 19:14:03 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#182