fix(site-runtime): republish loaded static overrides to children/stream and answer pending waits on stop (review 2026-10-21 01/SR-U1, 01/SR-M3)

SR-U1: children are created before the async override load lands, so their
trigger snapshots kept the deploy default forever after a restart/failover/
enable/sweep start. HandleOverridesLoaded now republishes every changed value
through PublishAndNotifyChildren(evaluateWaiters:false), and skips attributes
a static write touched before the load (newer than the stored row).
SR-M3: PostStop cancels each WaitForAttribute timeout and replies
'instance stopped' instead of leaving scripts to time out and timeouts to
dead-letter.
This commit is contained in:
Joseph Doherty
2026-10-22 06:31:15 -04:00
parent f5090fcd9e
commit e765700eac
3 changed files with 260 additions and 3 deletions
@@ -232,6 +232,10 @@ flowchart TD
- **SetAttribute** (for attributes with data source reference): Sends a write request to the Data Connection Layer. The DCL writes to the physical device. If the write fails (connection down, device rejection, timeout), the error is returned synchronously to the calling script for handling. On success, the existing subscription picks up the confirmed value from the device and sends it back as a value update, which then updates the in-memory value. The in-memory value is **not** optimistically updated.
- **SetAttribute** (for static attributes): Updates the in-memory value and **persists the override to local SQLite**. On restart (or failover), the Instance Actor loads persisted overrides on top of the deployed configuration, preserving runtime-modified values. A redeployment of the instance resets all persisted overrides to the new deployed configuration values.
> **Loaded overrides are republished to the children and the stream (review 2026-10-21 01/SR-U1).** The override load is asynchronous and lands *after* `PreStart` has already created the Script/Alarm Actors, whose trigger snapshots are seeded from the attribute set at that moment — the deploy **defaults**. Static attributes never re-emit a change on their own, so before this fix every trigger on an instance that restarted, failed over, was enabled or was started by the reconcile sweep evaluated an overridden attribute at its default indefinitely (an Expression alarm on `Mode == "Auto"` never raised), and the Debug View live stream showed the default, while `GetAttribute` answered correctly. The load now republishes each override that changed the in-memory value or quality as an ordinary `AttributeValueChanged` (quality from the load, no `SourceExecutionId`) through the same fan-out every change uses, with waiter evaluation **off** — nothing can legitimately be waiting on a replay. Consequence, accepted deliberately: a ValueChange/Conditional script keyed on an overridden static attribute fires once as the actor starts, exactly as one keyed on a data-sourced attribute already fires on that attribute's first DCL value. A static write that lands before the load is newer than the stored row, so the load skips that attribute rather than rolling it back. Overrides for attributes the deployed config no longer defines are still applied (unchanged) but not republished.
> **Stopping an instance answers its pending waits (review 2026-10-21 01/SR-M3).** `PostStop` cancels every registered `WaitForAttribute` timeout and replies `Matched=false, TimedOut=false, ErrorMessage="… abandoned: instance stopped"`, so a script parked in `Attributes.WaitAsync` across a redeploy/disable/delete learns why immediately instead of burning its whole wait plus the Ask timeout and reporting a time-out, and no `WaitForAttributeTimeout` lands in dead letters (a monitored health metric).
### Debug View Support
- On request from central (via Communication Layer), the Instance Actor provides a **snapshot** of all current attribute values and alarm states.
- Subsequent changes are delivered via the **SiteStreamManager** → **SiteStreamGrpcServer** → gRPC stream to central. The Instance Actor publishes attribute value and alarm state changes to the SiteStreamManager; it does not forward events directly to the Communication Layer.