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:
@@ -38,7 +38,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class GalaxyDriver
|
||||
: IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IRediscoverable, IHostConnectivityProbe, IAlarmSource, ISkippedTagReporter, IDisposable, IAsyncDisposable
|
||||
: IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IRediscoverable, IHostConnectivityProbe, IAlarmSource, IAlarmAcknowledgePreflight, ISkippedTagReporter, IDisposable, IAsyncDisposable
|
||||
{
|
||||
/// <summary>Authored tags this driver could not dial — a TagConfig with no usable
|
||||
/// <c>attributeRef</c> has no MXAccess address to bind.</summary>
|
||||
@@ -1966,6 +1966,21 @@ public sealed class GalaxyDriver
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Only the case decidable without I/O (lmxopcua#760): an anonymous session (no API key, so no key-mapped
|
||||
/// Galaxy identity can exist) on a driver with no <c>AlarmAck.FallbackUser</c>. A keyed session is let
|
||||
/// through — whether the key has an identity lives in the Secrets store, which must not be read under the
|
||||
/// SDK lock; <see cref="AcknowledgeAsync"/> still refuses it, and the host reverts the condition.
|
||||
/// </remarks>
|
||||
public string? GetAcknowledgeRefusal(string? apiKeyId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(apiKeyId) || (_options.AlarmAck?.HasFallback ?? false)) return null;
|
||||
return $"the session is anonymous (no API key) and Galaxy driver '{_driverInstanceId}' has no " +
|
||||
"AlarmAck.FallbackUser configured, so the acknowledge cannot be sent to Galaxy. Connect with an API key " +
|
||||
"that has a Galaxy ack identity (AdminUI /api-keys) or configure the driver's fallback ack account.";
|
||||
}
|
||||
|
||||
private static string PrincipalOf(AlarmAcknowledgeRequest ack) =>
|
||||
string.IsNullOrWhiteSpace(ack.OperatorUser) ? "anonymous" : ack.OperatorUser;
|
||||
|
||||
|
||||
+12
-3
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.MxGateway.Client;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
@@ -26,8 +27,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
/// A non-OK <see cref="ProtocolStatus"/> means the gateway never reached MXAccess (transport /
|
||||
/// dispatch / validation failure) and is surfaced as a thrown exception. A non-zero native ack
|
||||
/// return code (<c>hresult</c> — now the <c>AuthenticateUser</c> HRESULT or the AckMsg write status)
|
||||
/// means MXAccess rejected the ack; that is logged as a warning rather than thrown so a transient
|
||||
/// MXAccess hiccup doesn't block the operator workflow — the operator can retry.
|
||||
/// means MXAccess rejected the ack (a wrong password, an account without ack permission, a failed
|
||||
/// AckMsg write). It is logged and thrown as a <see cref="DriverOperationRefusedException"/>
|
||||
/// (lmxopcua#760): the host then reverts the OPC UA condition to unacknowledged instead of leaving it
|
||||
/// claiming an ack Galaxy never recorded, and the operator can retry. A refusal, not a fault — the
|
||||
/// gateway answered — so it neither retries nor counts toward the resilience breaker (a mis-keyed
|
||||
/// account must not open the breaker for every correctly-configured caller).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class GatewayGalaxyAlarmAcknowledger : IGalaxyAlarmAcknowledger
|
||||
@@ -98,7 +103,8 @@ internal sealed class GatewayGalaxyAlarmAcknowledger : IGalaxyAlarmAcknowledger
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the reply semantics: non-OK protocol status throws; a non-zero native code warns.
|
||||
/// Applies the reply semantics: non-OK protocol status throws; a non-zero native code warns and throws a
|
||||
/// <see cref="DriverOperationRefusedException"/> (lmxopcua#760).
|
||||
/// Every message is scrubbed of the password.
|
||||
/// </summary>
|
||||
/// <param name="reply">The gateway reply.</param>
|
||||
@@ -125,6 +131,9 @@ internal sealed class GatewayGalaxyAlarmAcknowledger : IGalaxyAlarmAcknowledger
|
||||
logger.LogWarning(
|
||||
"Galaxy AcknowledgeAlarm for {AlarmRef} as {GalaxyUser} returned native ack failure code {Hresult}.",
|
||||
alarmFullReference, credential.DisplayName, reply.Hresult);
|
||||
throw new DriverOperationRefusedException(
|
||||
$"MXAccess rejected the Galaxy acknowledge of '{alarmFullReference}' as '{credential.DisplayName}' "
|
||||
+ $"(native code 0x{reply.Hresult:X8}); check the account's password and Galaxy acknowledge permission.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user