feat(GWC-37,WRK-32): refresh live alarm watch lists
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Failing after 51s
ci / java (push) Successful in 2m8s
ci / portable (push) Failing after 1h1m54s

This commit is contained in:
Joseph Doherty
2026-08-25 11:46:07 -04:00
parent 1b11e89f29
commit 484028e721
22 changed files with 3961 additions and 1858 deletions
@@ -45,6 +45,7 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
private FailoverAlarmConsumer? failoverConsumer;
private EventHandler<AlarmProviderModeChange>? providerModeChangedHandler;
private string subscribeSessionId = string.Empty;
private string subscriptionExpression = string.Empty;
private bool disposed;
/// <summary>Initializes a new alarm command handler with the given event queue.</summary>
@@ -153,6 +154,7 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
}
subscribeSessionId = sessionId ?? string.Empty;
subscriptionExpression = command.SubscriptionExpression ?? string.Empty;
IMxAccessAlarmConsumer consumer = BuildConsumer(command)
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
@@ -171,7 +173,7 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
dispatcher = new AlarmDispatcher(consumer, sink, subscribeSessionId);
try
{
dispatcher.Subscribe(command.SubscriptionExpression ?? string.Empty);
dispatcher.Subscribe(subscriptionExpression);
}
catch
{
@@ -212,7 +214,8 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
return BuildStandby(watchList);
}
if (command.ForcedMode == AlarmProviderMode.Unspecified && watchList.Count > 0)
if (command.ForcedMode == AlarmProviderMode.Unspecified
&& (command.FallbackEnabled || watchList.Count > 0))
{
IMxAccessAlarmConsumer primary = consumerFactory()
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
@@ -230,6 +233,26 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
}
/// <inheritdoc />
public void UpdateWatchList(IReadOnlyList<AlarmSubtagTarget> watchList)
{
if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
if (watchList is null) throw new ArgumentNullException(nameof(watchList));
threadAffinityCheck?.Invoke();
lock (syncRoot)
{
if (dispatcher is null || failoverConsumer is null)
{
throw new InvalidOperationException(
"UpdateAlarmWatchList requires an active fallback-capable alarm subscription.");
}
IMxAccessAlarmConsumer replacement = BuildStandby(watchList);
failoverConsumer.ReplaceStandby(replacement, subscriptionExpression);
}
}
private IMxAccessAlarmConsumer BuildStandby(IReadOnlyList<AlarmSubtagTarget> watchList)
{
if (standbyFactory is not null)
@@ -276,6 +299,7 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
DetachProviderModeChanged();
toDispose = dispatcher;
dispatcher = null;
subscriptionExpression = string.Empty;
}
toDispose?.Dispose();
}
@@ -55,7 +55,7 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
}
private readonly IMxAccessAlarmConsumer primary;
private readonly IMxAccessAlarmConsumer standby;
private IMxAccessAlarmConsumer standby;
private readonly FailoverSettings settings;
private Active active = Active.Primary;
@@ -103,6 +103,36 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
/// </summary>
public AlarmProviderMode Mode => mode;
/// <summary>
/// Arms and attaches a replacement standby before atomically swapping
/// it into the current provider mode. Setup failure disposes only the
/// replacement and leaves the existing standby untouched.
/// </summary>
/// <param name="replacement">The newly built standby consumer.</param>
/// <param name="subscription">The retained subscription expression.</param>
public void ReplaceStandby(IMxAccessAlarmConsumer replacement, string subscription)
{
if (disposed) throw new ObjectDisposedException(nameof(FailoverAlarmConsumer));
if (replacement is null) throw new ArgumentNullException(nameof(replacement));
try
{
replacement.Subscribe(subscription ?? string.Empty);
replacement.AlarmTransitionEmitted += OnChildTransition;
}
catch
{
try { replacement.Dispose(); } catch { /* preserve setup failure */ }
throw;
}
IMxAccessAlarmConsumer previous = standby;
standby = replacement;
try { previous.AlarmTransitionEmitted -= OnChildTransition; } catch { /* best effort */ }
try { previous.Dispose(); } catch { /* replacement is already active */ }
}
/// <inheritdoc />
public void Subscribe(string subscription)
{
@@ -7,7 +7,8 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// <summary>
/// Per-session interface routing the worker's alarm IPC commands —
/// <c>SubscribeAlarmsCommand</c>, <c>AcknowledgeAlarmCommand</c>,
/// <c>QueryActiveAlarmsCommand</c>, <c>UnsubscribeAlarmsCommand</c> —
/// <c>UpdateAlarmWatchListCommand</c>, <c>QueryActiveAlarmsCommand</c>,
/// <c>UnsubscribeAlarmsCommand</c> —
/// to the underlying <see cref="AlarmDispatcher"/>. Production binding
/// is <see cref="AlarmCommandHandler"/>; tests substitute a fake.
/// </summary>
@@ -24,6 +25,13 @@ public interface IAlarmCommandHandler : IDisposable
/// <param name="sessionId">The session identifier.</param>
void Subscribe(SubscribeAlarmsCommand command, string sessionId);
/// <summary>
/// Replace the active auto-failover subscription's subtag standby
/// targets without restarting its primary provider.
/// </summary>
/// <param name="watchList">The complete replacement watch list.</param>
void UpdateWatchList(IReadOnlyList<AlarmSubtagTarget> watchList);
/// <summary>Tear down the active subscription. No-op if not subscribed.</summary>
void Unsubscribe();
@@ -158,6 +158,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
MxCommandKind.WriteSecured2Bulk => ExecuteWriteSecured2Bulk(command),
MxCommandKind.ReadBulk => ExecuteReadBulk(command),
MxCommandKind.SubscribeAlarms => ExecuteSubscribeAlarms(command),
MxCommandKind.UpdateAlarmWatchList => ExecuteUpdateAlarmWatchList(command),
MxCommandKind.UnsubscribeAlarms => ExecuteUnsubscribeAlarms(command),
MxCommandKind.AcknowledgeAlarm => ExecuteAcknowledgeAlarm(command),
MxCommandKind.AcknowledgeAlarmByName => ExecuteAcknowledgeAlarmByName(command),
@@ -866,6 +867,30 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
}
}
private MxCommandReply ExecuteUpdateAlarmWatchList(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.UpdateAlarmWatchList)
{
return CreateInvalidRequestReply(command, "UpdateAlarmWatchList command payload is required.");
}
if (alarmCommandHandler is null)
{
return CreateInvalidRequestReply(
command,
"UpdateAlarmWatchList requires an alarm command handler; the worker was constructed without one.");
}
try
{
alarmCommandHandler.UpdateWatchList(command.Command.UpdateAlarmWatchList.WatchList);
return CreateOkReply(command);
}
catch (Exception ex)
{
return CreateAlarmFailureReply(command, ex);
}
}
private MxCommandReply ExecuteAcknowledgeAlarm(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AcknowledgeAlarmCommand)