f54922f94c
Residuals of #760: 1. An ack routed to a disabled / not-yet-spawned driver, or to a stubbed child (driver does not run on this node, or its factory rejected the config), was dropped and left the condition Acknowledged. DriverHostActor now reverts it through the same NativeAlarmProjector.TryRevertAcknowledge path as a refused ack. 2. A native ack arriving while the host is Stale was dropped before the projector recorded it, so it could never be reverted. Stale now marks the whole NativeAlarmAckPreflightView unavailable (SetHostUnavailable), so the node manager refuses every native ack synchronously; BecomeSteady clears it. 3. (keyed sessions without a Galaxy identity) is not addressed. Tests: DriverHostActorNativeAlarmTests' client-ack test and the FOCAS respawn theory now start the host's own driver (AckingAlarmDriver; a factory building a real FocasDriver per spawn over per-instance fake CNCs, with a real config-change respawn) - both fail against the fix in their old shape. New: ack to a disabled driver reverts, ack to a stubbed driver reverts, a Stale host refuses through the pre-check view - all three fail with the fix mutated out. Runtime.Tests 943/0/3. Docs: AlarmTracking.md, .claude/rules/alarms.md. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
100 lines
3.7 KiB
C#
100 lines
3.7 KiB
C#
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
|
|
|
/// <summary>
|
|
/// A minimal <see cref="IDriver"/> + <see cref="IAlarmSource"/> that accepts every acknowledge and raises
|
|
/// nothing — the host's own running driver for tests that route a client acknowledge but drive the alarm
|
|
/// transitions themselves. Since lmxopcua#765 an ack to a driver that is not running (or is stubbed) is
|
|
/// reverted, so such a test must start the owning driver for the ack to stand.
|
|
/// </summary>
|
|
internal sealed class AckingAlarmDriver : IDriver, IAlarmSource
|
|
{
|
|
private int _acks;
|
|
private bool _alarmsSubscribed;
|
|
|
|
/// <inheritdoc />
|
|
public string DriverInstanceId { get; private set; } = string.Empty;
|
|
|
|
/// <inheritdoc />
|
|
public string DriverType { get; private set; } = string.Empty;
|
|
|
|
/// <summary>How many acknowledge requests the driver accepted.</summary>
|
|
public int Acks => Volatile.Read(ref _acks);
|
|
|
|
/// <summary>True once the driver child reached Connected and opened its alarm subscription.</summary>
|
|
public bool AlarmsSubscribed => Volatile.Read(ref _alarmsSubscribed);
|
|
|
|
/// <summary>Sets the identity once the factory is asked to create it.</summary>
|
|
/// <param name="id">The driver instance id.</param>
|
|
/// <param name="type">The driver type.</param>
|
|
public void Bind(string id, string type)
|
|
{
|
|
DriverInstanceId = id;
|
|
DriverType = type;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
/// <inheritdoc />
|
|
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
/// <inheritdoc />
|
|
public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
/// <inheritdoc />
|
|
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, LastError: null);
|
|
|
|
/// <inheritdoc />
|
|
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
|
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
|
{
|
|
Volatile.Write(ref _alarmsSubscribed, true);
|
|
return Task.FromResult<IAlarmSubscriptionHandle>(new Handle());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>
|
|
Task.CompletedTask;
|
|
|
|
/// <inheritdoc />
|
|
public Task AcknowledgeAsync(
|
|
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
|
{
|
|
Interlocked.Add(ref _acks, acknowledgements.Count);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public event EventHandler<AlarmEventArgs>? OnAlarmEvent
|
|
{
|
|
add { }
|
|
remove { }
|
|
}
|
|
|
|
private sealed class Handle : IAlarmSubscriptionHandle
|
|
{
|
|
public string DiagnosticId => "acking-alarm-sub";
|
|
}
|
|
}
|
|
|
|
/// <summary>Factory handing the host one shared <see cref="AckingAlarmDriver"/> for a single driver type.</summary>
|
|
/// <param name="supportedType">The driver type this factory creates.</param>
|
|
internal sealed class AckingAlarmDriverFactory(string supportedType) : IDriverFactory
|
|
{
|
|
/// <summary>The driver every spawn of <paramref name="supportedType"/> receives.</summary>
|
|
public AckingAlarmDriver Driver { get; } = new();
|
|
|
|
/// <inheritdoc />
|
|
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
|
|
{
|
|
if (!string.Equals(driverType, supportedType, StringComparison.Ordinal)) return null;
|
|
Driver.Bind(driverInstanceId, driverType);
|
|
return Driver;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyCollection<string> SupportedTypes => [supportedType];
|
|
}
|