Phase 7 follow-up #247 — Galaxy.Host historian writer + SQLite sink activation #194

Merged
dohertj2 merged 1 commits from phase-7-fu-247-galaxy-historian-writer into v2 2026-04-20 22:21:04 -04:00
Owner

Closes the historian leg of Phase 7. Scripted alarm transitions now batch-flow through the existing Galaxy.Host pipe + queue durably in a local SQLite store-and-forward when Galaxy is the registered driver, instead of being dropped into NullAlarmHistorianSink.

GalaxyHistorianWriter (Driver.Galaxy.Proxy.Ipc)

IAlarmHistorianWriter implementation. Translates AlarmHistorianEventHistorianAlarmEventDto (Stream D contract), batches via the existing GalaxyIpcClient.CallAsync round-trip on MessageKind.HistorianAlarmEventRequest / Response, maps per-event HistorianAlarmEventOutcomeDto bytes back to HistorianWriteOutcome (Ack / RetryPlease / PermanentFail) so the SQLite drain worker knows what to ack vs dead-letter vs retry. Empty-batch fast path. Pipe-level transport faults bubble up as GalaxyIpcException which the SQLite sink translates to whole-batch RetryPlease per its catch contract.

GalaxyProxyDriver implements IAlarmHistorianWriter

Marker interface lets Phase7Composer discover it via type check. WriteBatchAsync delegates to a thin GalaxyHistorianWriter wrapping the driver's existing _client. Throws InvalidOperationException if InitializeAsync hasn't connected yet — the SQLite drain worker treats that as a transient batch failure and retries.

Phase7Composer.ResolveHistorianSink

Replaces the injected sink dep when any registered driver implements IAlarmHistorianWriter. Constructs SqliteStoreAndForwardSink at %ProgramData%/OtOpcUa/alarm-historian-queue.db (falls back to %TEMP% when ProgramData unavailable), starts the 2s drain timer, owns the sink disposable for clean teardown. When no driver provides the writer, keeps the NullAlarmHistorianSink wired by Program.cs (#246).

DisposeAsync now disposes in the right order: bridge → engines → owned sink → injected fallback.

Tests — 7 new

GalaxyHistorianWriterMappingTests: ToDto round-trips every field; preserves null Comment; per-byte outcome enum mapping (Ack / RetryPlease / PermanentFail) via [Theory]; unknown byte throws; ctor null-guard. The IPC round-trip itself is covered by the live Host suite (task #240) which constructs a real pipe.

Server.Phase7 tests: 34/34 still pass; Galaxy.Proxy tests: 25/25 (+7 = 32 total).

Phase 7 production wiring chain — COMPLETE

  • #243 composition kernel
  • #245 scripted-alarm IReadable adapter
  • #244 driver bridge
  • #246 Program.cs wire-in
  • #247 this — Galaxy.Host historian writer + SQLite sink activation

What unblocks now: task #240 live OPC UA E2E smoke. With a Galaxy driver registered, scripted alarm transitions flow end-to-end through engine → SQLite queue → drain worker → Galaxy.Host IPC → Aveva Historian alarm schema. Without Galaxy, NullSink keeps the engines functional and the queue dormant.

Closes the historian leg of Phase 7. Scripted alarm transitions now batch-flow through the existing Galaxy.Host pipe + queue durably in a local SQLite store-and-forward when Galaxy is the registered driver, instead of being dropped into NullAlarmHistorianSink. ## `GalaxyHistorianWriter` (Driver.Galaxy.Proxy.Ipc) `IAlarmHistorianWriter` implementation. Translates `AlarmHistorianEvent` → `HistorianAlarmEventDto` (Stream D contract), batches via the existing `GalaxyIpcClient.CallAsync` round-trip on `MessageKind.HistorianAlarmEventRequest` / `Response`, maps per-event `HistorianAlarmEventOutcomeDto` bytes back to `HistorianWriteOutcome` (`Ack` / `RetryPlease` / `PermanentFail`) so the SQLite drain worker knows what to ack vs dead-letter vs retry. Empty-batch fast path. Pipe-level transport faults bubble up as `GalaxyIpcException` which the SQLite sink translates to whole-batch RetryPlease per its catch contract. ## `GalaxyProxyDriver implements IAlarmHistorianWriter` Marker interface lets `Phase7Composer` discover it via type check. `WriteBatchAsync` delegates to a thin `GalaxyHistorianWriter` wrapping the driver's existing `_client`. Throws `InvalidOperationException` if `InitializeAsync` hasn't connected yet — the SQLite drain worker treats that as a transient batch failure and retries. ## `Phase7Composer.ResolveHistorianSink` Replaces the injected sink dep when any registered driver implements `IAlarmHistorianWriter`. Constructs `SqliteStoreAndForwardSink` at `%ProgramData%/OtOpcUa/alarm-historian-queue.db` (falls back to `%TEMP%` when ProgramData unavailable), starts the 2s drain timer, owns the sink disposable for clean teardown. When no driver provides the writer, keeps the `NullAlarmHistorianSink` wired by Program.cs (#246). `DisposeAsync` now disposes in the right order: bridge → engines → owned sink → injected fallback. ## Tests — 7 new `GalaxyHistorianWriterMappingTests`: `ToDto` round-trips every field; preserves null Comment; per-byte outcome enum mapping (`Ack` / `RetryPlease` / `PermanentFail`) via `[Theory]`; unknown byte throws; ctor null-guard. The IPC round-trip itself is covered by the live Host suite (task #240) which constructs a real pipe. **Server.Phase7 tests: 34/34 still pass; Galaxy.Proxy tests: 25/25 (+7 = 32 total).** ## Phase 7 production wiring chain — COMPLETE - ✅ #243 composition kernel - ✅ #245 scripted-alarm IReadable adapter - ✅ #244 driver bridge - ✅ #246 Program.cs wire-in - ✅ **#247 this — Galaxy.Host historian writer + SQLite sink activation** What unblocks now: **task #240** live OPC UA E2E smoke. With a Galaxy driver registered, scripted alarm transitions flow end-to-end through engine → SQLite queue → drain worker → Galaxy.Host IPC → Aveva Historian alarm schema. Without Galaxy, NullSink keeps the engines functional and the queue dormant.
dohertj2 added 1 commit 2026-04-20 22:20:51 -04:00
Closes the historian leg of Phase 7. Scripted alarm transitions now batch-flow
through the existing Galaxy.Host pipe + queue durably in a local SQLite store-
and-forward when Galaxy is the registered driver, instead of being dropped into
NullAlarmHistorianSink.

## GalaxyHistorianWriter (Driver.Galaxy.Proxy.Ipc)

IAlarmHistorianWriter implementation. Translates AlarmHistorianEvent →
HistorianAlarmEventDto (Stream D contract), batches via the existing
GalaxyIpcClient.CallAsync round-trip on MessageKind.HistorianAlarmEventRequest /
Response, maps per-event HistorianAlarmEventOutcomeDto bytes back to
HistorianWriteOutcome (Ack/RetryPlease/PermanentFail) so the SQLite drain
worker knows what to ack vs dead-letter vs retry. Empty-batch fast path.
Pipe-level transport faults (broken pipe, host crash) bubble up as
GalaxyIpcException which the SQLite sink's drain worker translates to
whole-batch RetryPlease per its catch contract.

## GalaxyProxyDriver implements IAlarmHistorianWriter

Marker interface lets Phase7Composer discover it via type check at compose
time. WriteBatchAsync delegates to a thin GalaxyHistorianWriter wrapping the
driver's existing _client. Throws InvalidOperationException if InitializeAsync
hasn't connected yet — the SQLite drain worker treats that as a transient
batch failure and retries.

## Phase7Composer.ResolveHistorianSink

Replaces the injected sink dep when any registered driver implements
IAlarmHistorianWriter. Constructs SqliteStoreAndForwardSink at
%ProgramData%/OtOpcUa/alarm-historian-queue.db (falls back to %TEMP% when
ProgramData unavailable, e.g. dev), starts the 2s drain timer, owns the sink
disposable for clean teardown. When no driver provides the writer, keeps the
NullAlarmHistorianSink wired by Program.cs (#246).

DisposeAsync now also disposes the owned SQLite sink in the right order:
bridge → engines → owned sink → injected fallback.

## Tests — 7 new GalaxyHistorianWriterMappingTests

ToDto round-trips every field; preserves null Comment; per-byte outcome enum
mapping (Ack / RetryPlease / PermanentFail) via [Theory]; unknown byte throws;
ctor null-guard. The IPC round-trip itself is covered by the live Host suite
(task #240) which constructs a real pipe.

Server.Phase7 tests: 34/34 still pass; Galaxy.Proxy tests: 25/25 (+7 = 32 total).

## Phase 7 production wiring chain — COMPLETE
-  #243 composition kernel
-  #245 scripted-alarm IReadable adapter
-  #244 driver bridge
-  #246 Program.cs wire-in
-  #247 this — Galaxy.Host historian writer + SQLite sink activation

What unblocks now: task #240 live OPC UA E2E smoke. With a Galaxy driver
registered, scripted alarm transitions flow end-to-end through the engine →
SQLite queue → drain worker → Galaxy.Host IPC → Aveva Historian alarm schema.
Without Galaxy, NullSink keeps the engines functional and the queue dormant.
dohertj2 merged commit efdf04320a into v2 2026-04-20 22:21:04 -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#194