docs: record R4.3 measured idle-state status in hcal-roadmap

Update the M4 table row, one-glance status line, and M4 narrative note to
reflect R4.3: measured idle-state GetStoreForwardStatusAsync SHIPPED over
gRPC; active-SF magnitude + R4.2 revision edits stay deferred behind the
shared D2 storage-engine console-pipe wall.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
Joseph Doherty
2026-06-21 23:20:51 -04:00
parent 9db2864f70
commit e45c615a79
+9 -2
View File
@@ -273,7 +273,7 @@ Only if the use case demands them. Each is a real subsystem, not an op.
|---|---|---|---| |---|---|---|---|
| R4.1 | Store-and-forward | ✅ **SHIPPED (2026-06-21) — pragmatic durable outbox.** `AVEVA.Historian.Client.StoreForward`: `HistorianStoreForwardWriter` buffers historical-value + event writes to an `IHistorianOutboxStore` (`FileHistorianOutboxStore` = crash-durable atomic JSON-per-entry, FIFO by filename sequence, corrupt-file quarantine; `InMemoryHistorianOutboxStore` for tests) and replays them through an `IHistorianWriteSink` (default `HistorianClientWriteSink`). Background drain loop retries on reconnect; FIFO head-of-line blocking with optional `MaxDeliveryAttempts` dead-lettering; `DropOldest`/`Reject` overflow policy; `GetStatusAsync` snapshot (Pending/Storing/ErrorOccurred mirrors the server SF semantics). 12 unit tests (durability-across-restart, reconnect-drain, head-of-line order, dead-letter, overflow, background loop). **NOT** the bit-faithful native SF cache (`Forward*Snapshot` decode) — that stays deferred; pure client-side, no RE. | high; consider "good enough" | | R4.1 | Store-and-forward | ✅ **SHIPPED (2026-06-21) — pragmatic durable outbox.** `AVEVA.Historian.Client.StoreForward`: `HistorianStoreForwardWriter` buffers historical-value + event writes to an `IHistorianOutboxStore` (`FileHistorianOutboxStore` = crash-durable atomic JSON-per-entry, FIFO by filename sequence, corrupt-file quarantine; `InMemoryHistorianOutboxStore` for tests) and replays them through an `IHistorianWriteSink` (default `HistorianClientWriteSink`). Background drain loop retries on reconnect; FIFO head-of-line blocking with optional `MaxDeliveryAttempts` dead-lettering; `DropOldest`/`Reject` overflow policy; `GetStatusAsync` snapshot (Pending/Storing/ErrorOccurred mirrors the server SF semantics). 12 unit tests (durability-across-restart, reconnect-drain, head-of-line order, dead-letter, overflow, background loop). **NOT** the bit-faithful native SF cache (`Forward*Snapshot` decode) — that stays deferred; pure client-side, no RE. | high; consider "good enough" |
| R4.2 | Revision / edit writes | `AddRevisionValue(s)` go via the **non-WCF storage-engine pipe** (`STransactPipeClient2`) — separate transport RE | high | | R4.2 | Revision / edit writes | `AddRevisionValue(s)` go via the **non-WCF storage-engine pipe** (`STransactPipeClient2`) — separate transport RE | high |
| R4.3 | Real store-forward **status** | duplex push (`SetStoreForwardEvent`) or a decoded pull endpoint — see store-forward plan | medium | | R4.3 | Real store-forward **status** | ⚠️ **PARTIAL — measured idle-state SHIPPED (2026-06-21, gRPC); active-SF magnitude D2-blocked.** Re-scoped against the recovered 2023 R2 gRPC contract (the old "duplex push vs pull" risk is gone — `StorageService` exposes SF state as plain *pull* RPCs). Idle-baseline probe (`grpc-sf-status-probe`) against the live 2023 R2 server resolved the open handle question: the direct SF pull RPCs (`GetSFParameter` / `GetRemainingSnapshotsSize`) require the `OpenStorageConnection` storage-engine **console handle** and are **D2-gated** (same wall as R4.2 revisions), so `Storing`/`Pending`/`DataStored` magnitude is unreachable from a pure managed client. But `StatusService.GetHistorianConsoleStatus` IS reachable on the session string handle, so `GetStoreForwardStatusAsync` over gRPC now returns a **measured** idle-state — it actually contacts the server and reports `ErrorOccurred` when unreachable (vs the old blind all-false synthesis), live-verified + gated test. Non-gRPC keeps the synthesized fallback. Active-SF magnitude (path b) stays deferred behind D2 + needs an invasive force-SF capture to decode the console-status enum. See `docs/plans/store-forward-cache-reverse-engineering.md` §9. | medium (idle done; magnitude D2-blocked) |
| R4.4 | Multi-historian / redundancy | ✅ **SHIPPED (2026-06-21) — client-side orchestration.** `AVEVA.Historian.Client.Redundancy`: `HistorianRedundantClient` fronts N `IHistorianMember`s (default `HistorianClientMember` over `HistorianClient`) as one logical client. Reads fail over to the next member in priority order — streaming reads only fail over *before the first row* (mid-stream failures propagate to avoid dup/gap); writes fan out (`AllMembers`/`PreferredOnly`) with `All`/`Any` ack policy returning a per-member `HistorianRedundantWriteResult`. Per-member health (`FailureThreshold` demotion) + background watchdog (`CheckHealthAsync`/`PeriodicTimer`) restores recovered members; `GetStatus()` snapshot. Composes with R4.1: back a member's writes with a `HistorianStoreForwardWriter` for the pragmatic ReSyncTags equivalent (down member buffers + replays). 14 unit tests (failover order, mid-stream no-failover, ack policies, fanout modes, watchdog recovery, all-fail aggregation). Pure client-side, no server-side redundancy protocol, no RE. | medium | | R4.4 | Multi-historian / redundancy | ✅ **SHIPPED (2026-06-21) — client-side orchestration.** `AVEVA.Historian.Client.Redundancy`: `HistorianRedundantClient` fronts N `IHistorianMember`s (default `HistorianClientMember` over `HistorianClient`) as one logical client. Reads fail over to the next member in priority order — streaming reads only fail over *before the first row* (mid-stream failures propagate to avoid dup/gap); writes fan out (`AllMembers`/`PreferredOnly`) with `All`/`Any` ack policy returning a per-member `HistorianRedundantWriteResult`. Per-member health (`FailureThreshold` demotion) + background watchdog (`CheckHealthAsync`/`PeriodicTimer`) restores recovered members; `GetStatus()` snapshot. Composes with R4.1: back a member's writes with a `HistorianStoreForwardWriter` for the pragmatic ReSyncTags equivalent (down member buffers + replays). 14 unit tests (failover order, mid-stream no-failover, ack policies, fanout modes, watchdog recovery, all-fail aggregation). Pure client-side, no server-side redundancy protocol, no RE. | medium |
--- ---
@@ -322,6 +322,13 @@ event-send). M3/M4 as demand dictates.
> capture the `AddNonStreamValues` `btInput` VTQ buffer → golden-tested serializer → real > capture the `AddNonStreamValues` `btInput` VTQ buffer → golden-tested serializer → real
> commit+read-back → public `AddHistoricalValuesAsync`. The other levers are unchanged: R4.2 revision > commit+read-back → public `AddHistoricalValuesAsync`. The other levers are unchanged: R4.2 revision
> *edits* stay pipe-only even on gRPC, and M4 (SF / redundancy) is a HARD deferred subsystem. > *edits* stay pipe-only even on gRPC, and M4 (SF / redundancy) is a HARD deferred subsystem.
>
> **M4 update (2026-06-21):** R4.1 store-and-forward, R4.4 redundancy, and R4.3 *measured idle-state*
> SF status are all SHIPPED (pragmatic, client-side). What remains deferred sits behind the **D2
> storage-engine console-pipe wall**: R4.2 revision edits and the R4.3 *active-SF magnitude*
> (`Storing`/`Pending`/`DataStored`) — the SF pull RPCs that carry it need the console handle the
> managed client can't obtain. Decoding the active-SF console-status enum additionally needs an
> invasive force-SF capture on a sacrificial Historian.
## One-glance status ## One-glance status
@@ -331,4 +338,4 @@ event-send). M3/M4 as demand dictates.
| M1 cheap surface | TRIVIAL/BOUNDED | ML | most remaining read/config | ✅ **done** (reachable surface; rest bounded out) | | M1 cheap surface | TRIVIAL/BOUNDED | ML | most remaining read/config | ✅ **done** (reachable surface; rest bounded out) |
| M2 event send | CAPTURE | SM | headline write capability | ✅ **done** | | M2 event send | CAPTURE | SM | headline write capability | ✅ **done** |
| M3 historical writes | BOUNDED | M | backfill | ✅ **SHIPPED + LIVE-VALIDATED (2026-06-21)**`AddHistoricalValuesAsync` over gRPC = `HistoryService.AddStreamValues` ("ON" buffer) + tag-GUID resolve. Pure-managed SDK write read back live. All 5 analog types (Float/Double/Int2/Int4/UInt4). WCF still blocked (D2) | | M3 historical writes | BOUNDED | M | backfill | ✅ **SHIPPED + LIVE-VALIDATED (2026-06-21)**`AddHistoricalValuesAsync` over gRPC = `HistoryService.AddStreamValues` ("ON" buffer) + tag-GUID resolve. Pure-managed SDK write read back live. All 5 analog types (Float/Double/Int2/Int4/UInt4). WCF still blocked (D2) |
| M4 SF / revisions / redundancy | HARD | L×N | parity completeness | **R4.1 store-and-forward + R4.4 redundancy SHIPPED** (pragmatic, client-side, 2026-06-21); R4.2 revisions deferred (storage-engine-pipe wall), R4.3 real SF status deferred (RE-heavy, needs SF-active server) | | M4 SF / revisions / redundancy | HARD | L×N | parity completeness | **R4.1 store-and-forward + R4.4 redundancy + R4.3 measured idle-state SF status SHIPPED** (client-side, 2026-06-21); R4.2 revisions + R4.3 active-SF magnitude deferred behind the same D2 storage-engine-pipe wall (R4.3 magnitude also needs an SF-active server capture) |