Files
lmxopcua/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverOperationRefusedException.cs
T
Joseph Doherty 8e8d4cdf13 fix(alarms): surface a refused native alarm ack synchronously, revert one that fails later (lmxopcua#760)
The native-ack path is fire-and-forget after the Part 9 Acknowledge returns, so
a Galaxy ack refused for lack of a credential (#749), or rejected by MXAccess,
left the client with Good and the condition showing Acknowledged while Galaxy
was never acked. Both options from the issue, layered:

1. Synchronous refusal for what the driver can decide without I/O. New optional
   IAlarmAcknowledgePreflight (Core.Abstractions); GalaxyDriver refuses an
   anonymous session when the driver has no AlarmAck fallback account.
   DriverHostActor publishes a condition NodeId -> owning-driver snapshot into
   NativeAlarmAckPreflightView (lock-free, volatile); the node manager's new
   NativeAlarmAckPreflight seam, consulted after the authz and Primary gates,
   returns BadUserAccessDenied with the reason - nothing routed, applied or
   reported. A keyed session is not pre-checked (its identity lives in the
   Secrets store, which must not be read under the SDK lock).

2. Revert for everything else. RouteAlarmAck carries the condition NodeId;
   DriverInstanceActor replies AlarmAckFailed on a refusal, a backend failure,
   a driver without an alarm source, or an ack dropped while connecting /
   reconnecting. DriverHostActor re-projects the condition unacknowledged via
   NativeAlarmProjector.TryRevertAcknowledge, but only while the local ack is
   still pending - any driver transition projected since then wins. A disabled
   or unspawned driver is not reverted (unchanged).

GatewayGalaxyAlarmAcknowledger now throws DriverOperationRefusedException on a
non-zero native hresult (wrong password, no ack permission) instead of only
warning, so that case reverts too; as a refusal it neither retries nor trips
the breaker.

Tests (each fails with its fix mutated out): projector revert/supersede,
DriverHostActor refused + failed ack -> unacked AlarmStateUpdate (successful
ack control), preflight view snapshot, node-manager preflight unit test,
over-the-wire ConditionMethodEventTests (BadUserAccessDenied, no route, no
event; positive control), Galaxy preflight + hresult refusal.
Docs: Galaxy.md, AlarmTracking.md, ScriptedAlarms.md, .claude/rules/alarms.md.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 13:12:17 -05:00

23 lines
1.2 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Thrown when an operation is refused as a configuration / authorization verdict rather than failing
/// on a transport or device fault: by the driver itself before contacting its backend (e.g. no Galaxy
/// ack credential is available for the caller, lmxopcua#749), or by a backend that answered and
/// rejected it (e.g. MXAccess rejecting the operator account on a Galaxy acknowledge, lmxopcua#760).
/// </summary>
/// <remarks>
/// The resilience pipeline neither retries it nor counts it toward the circuit breaker: a refusal
/// says nothing about the backend's health, so letting five misconfigured acks open the breaker
/// would block every correctly-configured caller on the same host.
/// </remarks>
public sealed class DriverOperationRefusedException : InvalidOperationException
{
/// <summary>Initializes a new instance of the <see cref="DriverOperationRefusedException"/> class.</summary>
/// <param name="message">Why the operation was refused. Must never contain a secret.</param>
public DriverOperationRefusedException(string message)
: base(message)
{
}
}