fix(galaxy): picker-form alarm ack waits for the feed snapshot instead of dialling bare tag.Attr (#904)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
A picker-form native alarm (attributeRef tag.Attr) only learns the provider-qualified reference Galaxy!<Area>.tag.Attr from the alarm feed. After a host restart the condition can be restored from LocalDb before the feed's first snapshot reached the new driver, and an ack in that window dialled the bare tag.Attr, which the gateway's AcknowledgeAlarm refuses. The feed now exposes InitialSnapshotCompleted (set on snapshot_complete, after every snapshot entry has been raised). AcknowledgeAsync resolves the reference through ResolveAlarmAckReferenceAsync: when a picker-form tag has learned nothing yet and the feed is running, it waits for that snapshot (2 s, and the caller's token) and dials the reference the snapshot names, so the area always comes from the feed and a re-areaed object acks under its current area. No reference is guessed: if the snapshot does not name the alarm, or does not arrive in time, the authored reference is dialled as before and refused visibly. Chosen over the alternatives in the issue: a GR-browse area lookup can be stale against the runtime area and is not loaded for authored tags, and a gateway-side tag.Attr resolution needs a cross-repo deploy and would resolve from the same monitor cache the snapshot already carries. Tests: 5 driver cases + 1 feed case (fail with the call site / sentinel reverted), and a skip-gated live gate GalaxyPickerAckBeforeSnapshotLiveTests, run against windev's TEST Galaxy (TestMachine_015.TestAlarm003): an ack issued before the snapshot dials Galaxy!TestArea.TestMachine_015.TestAlarm003 and is confirmed; with the call site reverted it fails with the gateway's InvalidRequest refusal. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -227,7 +227,7 @@ Every (re)open of `StreamAlarms` replays the gateway's active-alarm snapshot. `G
|
||||
- **MXAccess status rows → OPC UA status (`StatusCodeMap.FromMxStatus`, #866/#872)**: an `Ok` row is graded by the item's OPC DA quality. Any other row's `detail` is an MxStatusDetail code (never a quality byte) and maps through the mxaccessgw vocabulary table (e.g. 1007 → `BadNotWritable`, 1008 → `BadUserAccessDenied`, 13/1005 → `BadTypeMismatch`, and the live-captured automation-object code **8001** — write rejected for type conversion → `BadTypeMismatch`). Unmapped details fall back to the category (`SecurityError` → `BadUserAccessDenied`, so 8018 is too; `CommunicationError` → `BadCommunicationError`; `ConfigurationError` → `BadConfigurationError`; else `Bad`), logged once per (detail, category). `detected_by` never decides the code. **`Pending` and `Warning` rows map to `Uncertain` (`0x40000000`) for any detail**: MXAccess sets success = -1 on Pending (accepted, in progress), so they are not failures. On a data change the value is published Uncertain. On a write reply the result is Uncertain, not Good, so the server reports the write as not committed and reverts the node; a commit MXAccess completes later arrives as a normal data change. Neither category has been seen on a data-change or write-reply row yet (mxaccessgw `docs/LiveCaptures-2026-11-13.md` §3), so the first one logs a warning.
|
||||
- **Undecodable values** (`galaxy.events.undecodable`): a value kind this driver has no CLR representation for is published as **`BadTypeMismatch` with a null value**, and the first occurrence of each distinct kind is logged. It is *not* published as a null carrying the event's own quality: an MXAccess event can be Good-quality while the payload is one we cannot map, and a good null is indistinguishable from a genuine null reading — a client would consume a value it never received as a real one, with the failure visible only in a log. Non-zero here means the gateway is sending a wire type `MxValueDecoder` has no mapping for and those readings are being lost; the fix is a decoder mapping, not a quality change.
|
||||
- **Initial values can overtake the subscribe reply (#859)** (`galaxy.events.early_parked` / `early_replayed` / `early_overflow`): mxaccessgw gives no reply-before-event ordering — the worker frames the SubscribeBulk reply on a thread-pool continuation while the STA is already pumping the OnDataChange that Advise queued, and the reply (unary Invoke) and the event (StreamEvents) are separate HTTP/2 streams anyway. An event for an unregistered handle is therefore only "stale after unsubscribe" when no subscribe is in flight. Every SubscribeBulk call site (`SubscribeAsync`, the subscribe-once `ReadAsync` — which waits only for the initial value and would otherwise read `BadTimeout` — the reconnect/replay-gap `ResubscribeAllAsync` (one window per subscription, not across the whole replay), `PerPlatformProbeWatcher`) opens `SubscriptionRegistry.BeginPendingSubscribe()` before the RPC and closes it right after registering, never later; inside the window unknown-handle events are parked, promoted by the `Register`/`Rebind` that binds the handle — map writes and promotion are one critical section, so no later event can resolve in between — and replayed on the pump's dispatch thread before any later event for that handle. Held events (parked + promoted-but-unreplayed) are bounded at 100 000 together. Unclaimed parked events are trimmed by **window sequence**, not only when every window has closed (concurrent reads keep some window open almost permanently): each window gets a monotonically increasing sequence when it opens, each parked event is stamped with the newest sequence, and once every window that opened at or before that stamp has closed the event is discarded — outside any window the stale drop is unchanged. **Sessions (#861):** item handles are per gateway session and the next session reuses the numbers. On reconnect `ReopenAsync` calls `SubscriptionRegistry.StartNewSession()` both before and after the gateway session swap (`ReplayAsync` does it only when no reopen ran), and the replay re-subscribes only subscriptions still holding the previous session's handles (one registered after the swap is already live). The pump is always restarted, even with nothing to replay. `StartNewSession()` discards everything held, **invalidates the reverse map** (subscriptions are kept; new-session events park until the replay's per-subscription `Rebind` promotes them, instead of resolving to the tag that owned the number before), and bumps a session number. Registration and rebind are refused for a stale session, so a `SubscribeAsync` whose reply straddled the reconnect is **re-issued on the new session** (up to 3 attempts), a subscribe-once read returns `BadCommunicationError`, and dead old-session handle numbers are never sent to `UnsubscribeBulk` (a read rebound by the replay releases the handles the registry holds, not its original ones); an old session's pump can no longer resolve through the map. Reads listen on the driver, not on a pump instance, so a read survives the reconnect's pump restart. `PerPlatformProbeWatcher` records the session with each probe handle: only current-session handles are published (fenced `TryUpsert`), unsubscribed or purged, and a seed reply that straddled a reconnect is re-issued. Purges are fenced by the session captured before the unsubscribe RPC. **Interim (I-3):** a completed `UnsubscribeBulk` purges anything parked for its handles, so a trailing event of a removed item is not promoted onto a reused handle number within one session. It cannot catch an event still in flight after the unsubscribe, and it also drops a genuine early value for a number reused that fast (a timeout rather than another tag's value). The complete fix needs a worker sequence on the SubscribeBulk reply so the driver can tell pre-subscribe events from post-subscribe ones — an optional cross-repo change in mxaccessgw.
|
||||
- **Native-alarm reference forms (#895)**: the gateway alarm feed names an alarm `Galaxy!<Area>.<tag>.<Attr>`, with `SourceObjectReference = <tag>.<Attr>`. An alarm tag may carry either form as its `attributeRef`: the plain `<tag>.<Attr>` the AdminUI Galaxy picker authors, or the provider-qualified form. The driver matches a transition on the full reference, then the source reference, then the full reference with its `Provider!Area.` prefix stripped. If none of those match an authored `attributeRef`, the area-free names are matched against the provider-qualified ones with their area removed, so a hand-authored `Galaxy!<OldArea>.<tag>.<Attr>` still resolves after Galaxy moves the object to another area. A reference with an empty area (`Galaxy!<tag>.<Attr>`) also matches, and so does the gateway's subtag provider, which sends only `<tag>` as the source reference. The driver remembers the latest full reference per tag, including transitions that arrive before an alarm subscription is registered, so that an ack dials what `AcknowledgeAlarm` accepts, since the gateway refuses a non-provider reference. A picker-form tag that has had no transition since the driver started still dials the bare `<tag>.<Attr>`. When the same alarm is authored in both forms, the provider-form tag gets the transitions while the area matches, and a warning is logged. Before #895, a picker-authored alarm never went active (found in the G6 live gate, `docs/drivers/evidence/galaxy-g6-e2e-2026-11-12.md`).
|
||||
- **Native-alarm reference forms (#895)**: the gateway alarm feed names an alarm `Galaxy!<Area>.<tag>.<Attr>`, with `SourceObjectReference = <tag>.<Attr>`. An alarm tag may carry either form as its `attributeRef`: the plain `<tag>.<Attr>` the AdminUI Galaxy picker authors, or the provider-qualified form. The driver matches a transition on the full reference, then the source reference, then the full reference with its `Provider!Area.` prefix stripped. If none of those match an authored `attributeRef`, the area-free names are matched against the provider-qualified ones with their area removed, so a hand-authored `Galaxy!<OldArea>.<tag>.<Attr>` still resolves after Galaxy moves the object to another area. A reference with an empty area (`Galaxy!<tag>.<Attr>`) also matches, and so does the gateway's subtag provider, which sends only `<tag>` as the source reference. The driver remembers the latest full reference per tag, including transitions that arrive before an alarm subscription is registered, so that an ack dials what `AcknowledgeAlarm` accepts, since the gateway refuses a non-provider reference. If a picker-form tag has had no transition since the driver started (for example right after a host restart restored the condition from LocalDb), the ack waits for the alarm feed's first active-alarm snapshot, up to 2 s and within the ack's own timeout, and then dials the full reference the snapshot names, in the object's current area (#904). The area is never guessed. If the snapshot does not name the alarm, or does not arrive in time, the bare `<tag>.<Attr>` is dialled as before, the gateway refuses it, and the host reverts the ack. When the same alarm is authored in both forms, the provider-form tag gets the transitions while the area matches, and a warning is logged. Before #895, a picker-authored alarm never went active (found in the G6 live gate, `docs/drivers/evidence/galaxy-g6-e2e-2026-11-12.md`).
|
||||
- **Duplicate `attributeRef` (#605 / 06-S-13)**: two authored RawPaths dialling the same Galaxy attribute are both dialled, but the reverse map is first-wins, so value fan-out and alarm `ConditionId` surface under the first RawPath only. The loser is logged at Warning and recorded in the skipped-tag ledger with the winner path in the reason.
|
||||
- **Subscribe-once reads are bounded (#605 / 06-S-3)**: `ReadAsync` on the production path (SubscribeBulk, first event, Unsubscribe) carries an internal 30 s deadline in addition to the caller token; slots that never publish come back `BadTimeout`, so `CancellationToken.None` can no longer hang on a silent tag.
|
||||
- **Supervisory advise is retried after a thrown failure (#605 / 06-S-12)**: the writer marks an item handle advised before the call; a thrown advise (not just a non-OK status) now unmarks it, fails the write closed and re-advises on the next write.
|
||||
|
||||
Reference in New Issue
Block a user