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
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown by a driver that deliberately refuses an operation before contacting its backend — a
|
||||
/// configuration / authorization verdict (e.g. no Galaxy ack credential is available for the
|
||||
/// caller, lmxopcua#749), not a transport or device fault.
|
||||
/// 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
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Optional companion to <see cref="IAlarmSource"/>: lets the OPC UA Part 9 <c>Acknowledge</c> method
|
||||
/// refuse, with a synchronous status, an acknowledge the driver already knows it would refuse
|
||||
/// (lmxopcua#760). The native ack path is otherwise fire-and-forget after the method returns, so without
|
||||
/// this the client sees <c>Good</c> for an acknowledge that never reached the upstream alarm system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Called on an OPC UA server thread, under the SDK node-manager lock: an implementation must answer from
|
||||
/// state it already holds — no I/O, no secret lookups, no blocking. It is a best-effort pre-check for
|
||||
/// static misconfiguration only; an acknowledge it lets through can still fail later (that path reverts
|
||||
/// the condition to unacknowledged instead).
|
||||
/// </remarks>
|
||||
public interface IAlarmAcknowledgePreflight
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns why an acknowledge from a session that authenticated with <paramref name="apiKeyId"/> would
|
||||
/// be refused, or <c>null</c> when it would proceed or the driver cannot tell without I/O.
|
||||
/// </summary>
|
||||
/// <param name="apiKeyId">The session's API key id, or null for an anonymous session.</param>
|
||||
/// <returns>A refusal reason safe to show the OPC UA client (never a secret), or null.</returns>
|
||||
string? GetAcknowledgeRefusal(string? apiKeyId);
|
||||
}
|
||||
Reference in New Issue
Block a user