worker(alarms): route ForcedMode/watch-list/failover via AlarmCommandHandler; emit provider-mode-changed event
This commit is contained in:
@@ -372,11 +372,11 @@ public sealed class AlarmCommandExecutorTests
|
||||
public string? LastFilterPrefix { get; private set; }
|
||||
|
||||
/// <summary>Records a subscription.</summary>
|
||||
/// <param name="subscription">The subscription expression.</param>
|
||||
/// <param name="command">The subscribe-alarms command.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
public void Subscribe(string subscription, string sessionId)
|
||||
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
|
||||
{
|
||||
LastSubscription = subscription;
|
||||
LastSubscription = command.SubscriptionExpression;
|
||||
LastSessionId = sessionId;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
new MxAccessEventQueue(),
|
||||
() => consumer);
|
||||
|
||||
handler.Subscribe(@"\\HOST\Galaxy!Area", "session-1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!Area" }, "session-1");
|
||||
|
||||
Assert.True(handler.IsSubscribed);
|
||||
Assert.Equal(@"\\HOST\Galaxy!Area", consumer.LastSubscription);
|
||||
@@ -36,9 +36,9 @@ public sealed class AlarmCommandHandlerTests
|
||||
new MxAccessEventQueue(),
|
||||
() => consumer);
|
||||
|
||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => handler.Subscribe(@"\\HOST\Galaxy!B", "s1"));
|
||||
() => handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!B" }, "s1"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +63,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
() => consumer);
|
||||
|
||||
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(
|
||||
() => handler.Subscribe(@"\\HOST\Galaxy!A", "s1"));
|
||||
() => handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1"));
|
||||
Assert.Contains("simulated wnwrap subscribe failure", exception.Message);
|
||||
Assert.False(handler.IsSubscribed);
|
||||
Assert.True(consumer.Disposed);
|
||||
@@ -77,7 +77,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||
new MxAccessEventQueue(),
|
||||
() => consumer);
|
||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
|
||||
handler.Unsubscribe();
|
||||
|
||||
@@ -104,7 +104,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||
new MxAccessEventQueue(),
|
||||
() => consumer);
|
||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
|
||||
Guid g = Guid.NewGuid();
|
||||
int rc = handler.Acknowledge(g, "c", "u", "n", "d", "F");
|
||||
@@ -149,7 +149,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||
new MxAccessEventQueue(),
|
||||
() => consumer);
|
||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
|
||||
IReadOnlyList<ActiveAlarmSnapshot> snapshots = handler.QueryActive(null);
|
||||
|
||||
@@ -173,7 +173,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||
new MxAccessEventQueue(),
|
||||
() => consumer);
|
||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
|
||||
IReadOnlyList<ActiveAlarmSnapshot> filtered = handler.QueryActive("Galaxy!AreaA");
|
||||
|
||||
@@ -189,13 +189,13 @@ public sealed class AlarmCommandHandlerTests
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||
new MxAccessEventQueue(),
|
||||
() => consumer);
|
||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
|
||||
handler.Dispose();
|
||||
|
||||
Assert.True(consumer.Disposed);
|
||||
Assert.Throws<ObjectDisposedException>(
|
||||
() => handler.Subscribe("x", "y"));
|
||||
() => handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = "x" }, "y"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,7 +218,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
// factory is invoked. We tally invocation counts after each call so
|
||||
// that a missed guard surfaces as the diagnostic count, not a generic
|
||||
// "Subscribe should have failed".
|
||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
||||
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
Assert.Equal(1, guardInvocations);
|
||||
|
||||
handler.Acknowledge(Guid.NewGuid(), "c", "u", "n", "d", "F");
|
||||
@@ -254,7 +254,7 @@ public sealed class AlarmCommandHandlerTests
|
||||
|
||||
// Subscribe: guard runs before the dispatcher is constructed.
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => handler.Subscribe(@"\\HOST\Galaxy!A", "s1"));
|
||||
() => handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1"));
|
||||
|
||||
// To exercise the other entry points we need a subscribed handler.
|
||||
// Construct a parallel handler with a passing guard, then swap in a
|
||||
@@ -273,6 +273,132 @@ public sealed class AlarmCommandHandlerTests
|
||||
Assert.Throws<InvalidOperationException>(() => handler.Unsubscribe());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-9: ForcedMode=Subtag builds a subtag consumer (via the
|
||||
/// injected standby factory) and advises it — the primary
|
||||
/// (alarmmgr) consumer is NOT created.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Subscribe_WithForcedSubtagMode_BuildsStandbyConsumerOnly()
|
||||
{
|
||||
FakeConsumer primary = new FakeConsumer();
|
||||
FakeConsumer standby = new FakeConsumer();
|
||||
IReadOnlyList<AlarmSubtagTarget>? capturedWatchList = null;
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||
new MxAccessEventQueue(),
|
||||
() => primary,
|
||||
threadAffinityCheck: null,
|
||||
comFactory: null,
|
||||
standbyFactory: watch =>
|
||||
{
|
||||
capturedWatchList = watch;
|
||||
return standby;
|
||||
});
|
||||
|
||||
SubscribeAlarmsCommand command = new SubscribeAlarmsCommand
|
||||
{
|
||||
SubscriptionExpression = @"\\HOST\Galaxy!Area",
|
||||
ForcedMode = AlarmProviderMode.Subtag,
|
||||
};
|
||||
command.WatchList.Add(new AlarmSubtagTarget { AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi" });
|
||||
|
||||
handler.Subscribe(command, "s1");
|
||||
|
||||
Assert.True(handler.IsSubscribed);
|
||||
Assert.Equal(@"\\HOST\Galaxy!Area", standby.LastSubscription); // standby advised
|
||||
Assert.Null(primary.LastSubscription); // primary never built
|
||||
Assert.NotNull(capturedWatchList);
|
||||
Assert.Single(capturedWatchList!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-9: ForcedMode=Unspecified + a non-empty watch list builds a
|
||||
/// failover composite (primary + subtag standby). Forcing the primary
|
||||
/// to fail on subscribe with a threshold of 1 drives the composite to
|
||||
/// switch to the subtag provider, which must enqueue an
|
||||
/// OnAlarmProviderModeChanged event carrying mode=Subtag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Subscribe_AutoModeWithWatchList_FailoverModeChange_EnqueuesProviderModeChangedEvent()
|
||||
{
|
||||
FakeConsumer primary = new FakeConsumer { ThrowOnSubscribe = true };
|
||||
FakeConsumer standby = new FakeConsumer();
|
||||
MxAccessEventQueue queue = new MxAccessEventQueue();
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||
queue,
|
||||
() => primary,
|
||||
threadAffinityCheck: null,
|
||||
comFactory: null,
|
||||
standbyFactory: _ => standby);
|
||||
|
||||
SubscribeAlarmsCommand command = new SubscribeAlarmsCommand
|
||||
{
|
||||
SubscriptionExpression = @"\\HOST\Galaxy!Area",
|
||||
ForcedMode = AlarmProviderMode.Unspecified,
|
||||
Failover = new AlarmFailoverConfig
|
||||
{
|
||||
ConsecutiveFailureThreshold = 1,
|
||||
FailbackProbeIntervalSeconds = 1,
|
||||
FailbackStableProbes = 1,
|
||||
},
|
||||
};
|
||||
command.WatchList.Add(new AlarmSubtagTarget { AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi" });
|
||||
|
||||
// Subscribe: standby is armed cleanly; the primary subscribe throws and,
|
||||
// at threshold 1, the failover composite switches to standby and raises
|
||||
// ProviderModeChanged. The handler enqueues the proto event.
|
||||
handler.Subscribe(command, "s1");
|
||||
|
||||
IReadOnlyList<WorkerEvent> drained = queue.Drain(0);
|
||||
Assert.Single(drained);
|
||||
MxEvent evt = drained[0].Event;
|
||||
Assert.Equal(MxEventFamily.OnAlarmProviderModeChanged, evt.Family);
|
||||
Assert.Equal("s1", evt.SessionId);
|
||||
Assert.NotNull(evt.OnAlarmProviderModeChanged);
|
||||
Assert.Equal(AlarmProviderMode.Subtag, evt.OnAlarmProviderModeChanged.Mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-9: a non-failover subscribe (alarmmgr-only) never enqueues a
|
||||
/// provider-mode-changed event, and a subsequent Unsubscribe detaches
|
||||
/// the handler so no event leaks.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Subscribe_AlarmmgrOnly_DoesNotEnqueueProviderModeChangedEvent()
|
||||
{
|
||||
FakeConsumer consumer = new FakeConsumer();
|
||||
MxAccessEventQueue queue = new MxAccessEventQueue();
|
||||
AlarmCommandHandler handler = new AlarmCommandHandler(queue, () => consumer);
|
||||
|
||||
handler.Subscribe(
|
||||
new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||
handler.Unsubscribe();
|
||||
|
||||
Assert.Empty(queue.Drain(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-9: the mapper builds a well-formed OnAlarmProviderModeChanged
|
||||
/// MxEvent — correct family and populated body fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Mapper_CreateOnAlarmProviderModeChanged_PopulatesFamilyAndBody()
|
||||
{
|
||||
MxAccessEventMapper mapper = new MxAccessEventMapper();
|
||||
DateTime at = new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
MxEvent evt = mapper.CreateOnAlarmProviderModeChanged(
|
||||
"session-7", AlarmProviderMode.Subtag, "primary PollOnce failed", unchecked((int)0x80004005), at);
|
||||
|
||||
Assert.Equal(MxEventFamily.OnAlarmProviderModeChanged, evt.Family);
|
||||
Assert.Equal("session-7", evt.SessionId);
|
||||
Assert.NotNull(evt.OnAlarmProviderModeChanged);
|
||||
Assert.Equal(AlarmProviderMode.Subtag, evt.OnAlarmProviderModeChanged.Mode);
|
||||
Assert.Equal("primary PollOnce failed", evt.OnAlarmProviderModeChanged.Reason);
|
||||
Assert.Equal(unchecked((int)0x80004005), evt.OnAlarmProviderModeChanged.Hresult);
|
||||
Assert.Equal(at, evt.OnAlarmProviderModeChanged.At.ToDateTime());
|
||||
}
|
||||
|
||||
private static MxAlarmSnapshotRecord NewRecord(string provider, string group, string tag)
|
||||
{
|
||||
return new MxAlarmSnapshotRecord
|
||||
|
||||
@@ -200,7 +200,7 @@ public sealed class MxAccessStaSessionTests
|
||||
factory,
|
||||
eventSink,
|
||||
new MxAccessEventQueue(),
|
||||
(_eq, _affinity) => handler);
|
||||
(_eq, _affinity, _comFactory) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
@@ -279,7 +279,7 @@ public sealed class MxAccessStaSessionTests
|
||||
factory,
|
||||
eventSink,
|
||||
new MxAccessEventQueue(),
|
||||
(_eq, _affinity) => handler);
|
||||
(_eq, _affinity, _comFactory) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
@@ -320,7 +320,7 @@ public sealed class MxAccessStaSessionTests
|
||||
factory,
|
||||
eventSink,
|
||||
new MxAccessEventQueue(),
|
||||
(_eq, _affinity) => handler);
|
||||
(_eq, _affinity, _comFactory) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
@@ -369,7 +369,7 @@ public sealed class MxAccessStaSessionTests
|
||||
factory,
|
||||
eventSink,
|
||||
eventQueue,
|
||||
(_eq, _affinity) => handler);
|
||||
(_eq, _affinity, _comFactory) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
@@ -416,7 +416,7 @@ public sealed class MxAccessStaSessionTests
|
||||
factory,
|
||||
eventSink,
|
||||
eventQueue,
|
||||
(_eq, _affinity) => handler);
|
||||
(_eq, _affinity, _comFactory) => handler);
|
||||
|
||||
await session.StartAsync("session-1", workerProcessId: 1);
|
||||
|
||||
@@ -496,12 +496,12 @@ public sealed class MxAccessStaSessionTests
|
||||
}
|
||||
|
||||
/// <summary>Subscribes to alarm events.</summary>
|
||||
/// <param name="subscription">The subscription descriptor.</param>
|
||||
/// <param name="command">The subscribe-alarms command.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
public void Subscribe(string subscription, string sessionId)
|
||||
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
|
||||
{
|
||||
IsSubscribed = true;
|
||||
LastSubscription = subscription;
|
||||
LastSubscription = command.SubscriptionExpression;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from alarm events.</summary>
|
||||
|
||||
Reference in New Issue
Block a user