docs: record value-pipeline batching (plan 2026-08-15)
- docs/v2/Galaxy.Performance.md gains a "Value-path batching (2026-08-15)" section: the batch seams (IBatchSubscribable / PollGroupEngine onBatch -> AttributeValueBatchPublished -> AttributeValueBatchUpdate -> sink WriteValues with 512-chunked single-lock batches in the node manager), the invariants (#519 status codes verbatim, per-ITEM metering, Modbus deadband honored via the driver-filtered buffer, the ValueWriteBatchException partial-failure contract), the RealmKey re-key, and the pinned dispatcher's throughput 1 -> 100. - The plan doc gains a "Follow-ups (recorded, not implemented)" section: the PollGroupEngine unconditional-collection trap (a future per-tag filter in S7/AbCip/FOCAS would silently reintroduce the deadband-bypass bug), a two-level driverId -> (rawPath -> refs) routing map, null/empty guard consistency on the internal batch messages, a possible DriverHostActorLiveValueTests merge, and the pre-existing aborted-cycle tail-loss on the three unbuffered drivers. - tasks.json: all 13 tasks completed. CLAUDE.md needed no edit: its only statement in this area ("One driver publish for a RawPath fans (in DriverHostActor) to the raw NodeId AND every referencing UNS NodeId") is still true — batching changed the message granularity, not the fan-out semantics. Verification: dotnet build ZB.MOM.WW.OtOpcUa.slnx clean (0 errors, no new warning classes, zero warnings in any production file). 9 unit projects green, 3470 passed / 5 skipped / 0 failed: Core.Abstractions 305, Core 351+1s, Commons 348, Runtime 575+3s, OpcUaServer 424+1s, Modbus 399, S7 302, AbCip 410, FOCAS 356. No DB-backed suite was run — tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests does not exist, and nothing this branch touches is covered by the SQL-backed projects.
This commit is contained in:
@@ -591,3 +591,41 @@ Steps:
|
||||
- **No MQTT / MTConnect / Sparkplug / Galaxy / OpcUaClient batch implementations** — they flow through the single-item-batch path unchanged.
|
||||
- **No change to alarm paths** (`AttributeAlarmPublished`, `AlarmStateUpdate`) — value path only.
|
||||
- **No PollGroupEngine cadence change** (fixed-rate scheduling is a separate item).
|
||||
|
||||
---
|
||||
|
||||
## Follow-ups (recorded, not implemented)
|
||||
|
||||
Raised during Tasks 1–12 review and deliberately left out of this plan's scope. None is a defect in the
|
||||
delivered work; each is a place where the next change in the area could go wrong quietly.
|
||||
|
||||
1. **`PollGroupEngine` collects batch items unconditionally — the batch event is only as trustworthy as
|
||||
the driver's `onChange`.** The engine has no knowledge of a driver's publish filter, so its items list
|
||||
is pre-filter by construction. That is safe *today* only because S7, AbCip and FOCAS forward every
|
||||
`onChange` unconditionally, and Modbus — the one driver with a deadband — ignores the engine's list and
|
||||
raises from its own filtered buffer. The trap: a future per-tag filter added to S7/AbCip/FOCAS's
|
||||
`onChange` would silently reintroduce the exact deadband-bypass class of bug fixed for Modbus in
|
||||
`cd4ce305`, with a green test suite (the two events would simply disagree, and only a consumer
|
||||
preferring the batch would notice). Consider changing the engine's `onChange` contract to
|
||||
`Func<..., bool>` so the engine collects an item only when the driver reports it published — making the
|
||||
filter structurally impossible to skip rather than a convention each driver must remember.
|
||||
2. **Two-level `driverId → (rawPath → refs)` routing map in `DriverHostActor`.** The fan-out map is keyed
|
||||
by a composite of driver id + raw path, so resolving a batch re-hashes the driver id once per item even
|
||||
though a batch is by definition single-driver. Nesting the map would hash the driver id once per batch
|
||||
and the raw path once per item. Measure first — batches are small enough that this may be noise
|
||||
relative to the message-count win already banked.
|
||||
3. **Null/empty guard consistency on the internal batch messages.** The guards on
|
||||
`AttributeValueBatchPublished` / `AttributeValueBatchUpdate` grew per task and are not uniform — some
|
||||
seams drop an empty batch early, others tolerate it downstream. These are internal messages with
|
||||
trusted senders so nothing is unsafe today; worth one pass to make the contract identical at every hop
|
||||
and stated once, rather than re-derived per reader.
|
||||
4. **Possible merge of `DriverHostActorLiveValueTests` into the batch suite.** After Task 8 both files
|
||||
drive the same fan-out through the same message; the split is now historical rather than meaningful.
|
||||
Merging would remove a genuine ambiguity about where a new fan-out test belongs — but check first that
|
||||
the older suite's per-item comms-loss cases really are covered, since those enter via a different seam.
|
||||
5. **Aborted-cycle tail-loss when a per-tag handler throws, for the three unbuffered drivers.** If a
|
||||
consumer's `onChange` throws mid-cycle in S7/AbCip/FOCAS, the cycle aborts and the items already
|
||||
collected for that cycle never reach a batch raise. **Pre-existing and narrower than the Modbus case**
|
||||
(Modbus's own buffer is per-handle and is dropped on unsubscribe/dispose, so it cannot leak), and it
|
||||
predates this plan — the per-tag path lost the same tail before batching existed. Recorded so it is not
|
||||
mistaken for a regression introduced here.
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"planPath": "docs/plans/2026-08-15-value-pipeline-batching.md",
|
||||
"tasks": [
|
||||
{"id": 1, "subject": "Task 1: Batch contract types in Core.Abstractions", "status": "pending"},
|
||||
{"id": 2, "subject": "Task 2: PollGroupEngine batch callback", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 3, "subject": "Task 3a: Modbus + S7 raise the batch event", "status": "pending", "blockedBy": [2]},
|
||||
{"id": 4, "subject": "Task 3b: AbCip + FOCAS raise the batch event", "status": "pending", "blockedBy": [2]},
|
||||
{"id": 5, "subject": "Task 4: DriverInstanceActor batch forwarding", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 6, "subject": "Task 5: ValueWrite + sink WriteValues default method", "status": "pending"},
|
||||
{"id": 7, "subject": "Task 6: OtOpcUaNodeManager batched write under one lock", "status": "pending", "blockedBy": [6]},
|
||||
{"id": 8, "subject": "Task 7: OpcUaPublishActor batch message", "status": "pending", "blockedBy": [6]},
|
||||
{"id": 9, "subject": "Task 8: DriverHostActor batched fan-out", "status": "pending", "blockedBy": [5, 8]},
|
||||
{"id": 10, "subject": "Task 9: DependencyMuxActor batch receive", "status": "pending", "blockedBy": [9]},
|
||||
{"id": 11, "subject": "Task 10: Raise pinned-dispatcher throughput", "status": "pending"},
|
||||
{"id": 12, "subject": "Task 11: Re-key node-manager maps", "status": "pending", "blockedBy": [7]},
|
||||
{"id": 13, "subject": "Task 12: Full verification + docs", "status": "pending", "blockedBy": [3, 4, 9, 10, 11, 12]}
|
||||
{"id": 1, "subject": "Task 1: Batch contract types in Core.Abstractions", "status": "completed"},
|
||||
{"id": 2, "subject": "Task 2: PollGroupEngine batch callback", "status": "completed", "blockedBy": [1]},
|
||||
{"id": 3, "subject": "Task 3a: Modbus + S7 raise the batch event", "status": "completed", "blockedBy": [2]},
|
||||
{"id": 4, "subject": "Task 3b: AbCip + FOCAS raise the batch event", "status": "completed", "blockedBy": [2]},
|
||||
{"id": 5, "subject": "Task 4: DriverInstanceActor batch forwarding", "status": "completed", "blockedBy": [1]},
|
||||
{"id": 6, "subject": "Task 5: ValueWrite + sink WriteValues default method", "status": "completed"},
|
||||
{"id": 7, "subject": "Task 6: OtOpcUaNodeManager batched write under one lock", "status": "completed", "blockedBy": [6]},
|
||||
{"id": 8, "subject": "Task 7: OpcUaPublishActor batch message", "status": "completed", "blockedBy": [6]},
|
||||
{"id": 9, "subject": "Task 8: DriverHostActor batched fan-out", "status": "completed", "blockedBy": [5, 8]},
|
||||
{"id": 10, "subject": "Task 9: DependencyMuxActor batch receive", "status": "completed", "blockedBy": [9]},
|
||||
{"id": 11, "subject": "Task 10: Raise pinned-dispatcher throughput", "status": "completed"},
|
||||
{"id": 12, "subject": "Task 11: Re-key node-manager maps", "status": "completed", "blockedBy": [7]},
|
||||
{"id": 13, "subject": "Task 12: Full verification + docs", "status": "completed", "blockedBy": [3, 4, 9, 10, 11, 12]}
|
||||
],
|
||||
"lastUpdated": "2026-08-15T00:00:00Z"
|
||||
"lastUpdated": "2026-08-15T17:08:35Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user