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; }
|
public string? LastFilterPrefix { get; private set; }
|
||||||
|
|
||||||
/// <summary>Records a subscription.</summary>
|
/// <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>
|
/// <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;
|
LastSessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
() => consumer);
|
() => consumer);
|
||||||
|
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!Area", "session-1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!Area" }, "session-1");
|
||||||
|
|
||||||
Assert.True(handler.IsSubscribed);
|
Assert.True(handler.IsSubscribed);
|
||||||
Assert.Equal(@"\\HOST\Galaxy!Area", consumer.LastSubscription);
|
Assert.Equal(@"\\HOST\Galaxy!Area", consumer.LastSubscription);
|
||||||
@@ -36,9 +36,9 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
() => consumer);
|
() => consumer);
|
||||||
|
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||||
Assert.Throws<InvalidOperationException>(
|
Assert.Throws<InvalidOperationException>(
|
||||||
() => handler.Subscribe(@"\\HOST\Galaxy!B", "s1"));
|
() => handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!B" }, "s1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -63,7 +63,7 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
() => consumer);
|
() => consumer);
|
||||||
|
|
||||||
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(
|
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.Contains("simulated wnwrap subscribe failure", exception.Message);
|
||||||
Assert.False(handler.IsSubscribed);
|
Assert.False(handler.IsSubscribed);
|
||||||
Assert.True(consumer.Disposed);
|
Assert.True(consumer.Disposed);
|
||||||
@@ -77,7 +77,7 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
() => consumer);
|
() => consumer);
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||||
|
|
||||||
handler.Unsubscribe();
|
handler.Unsubscribe();
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
() => consumer);
|
() => consumer);
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||||
|
|
||||||
Guid g = Guid.NewGuid();
|
Guid g = Guid.NewGuid();
|
||||||
int rc = handler.Acknowledge(g, "c", "u", "n", "d", "F");
|
int rc = handler.Acknowledge(g, "c", "u", "n", "d", "F");
|
||||||
@@ -149,7 +149,7 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
() => consumer);
|
() => consumer);
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||||
|
|
||||||
IReadOnlyList<ActiveAlarmSnapshot> snapshots = handler.QueryActive(null);
|
IReadOnlyList<ActiveAlarmSnapshot> snapshots = handler.QueryActive(null);
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
() => consumer);
|
() => consumer);
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||||
|
|
||||||
IReadOnlyList<ActiveAlarmSnapshot> filtered = handler.QueryActive("Galaxy!AreaA");
|
IReadOnlyList<ActiveAlarmSnapshot> filtered = handler.QueryActive("Galaxy!AreaA");
|
||||||
|
|
||||||
@@ -189,13 +189,13 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
AlarmCommandHandler handler = new AlarmCommandHandler(
|
AlarmCommandHandler handler = new AlarmCommandHandler(
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
() => consumer);
|
() => consumer);
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||||
|
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
|
|
||||||
Assert.True(consumer.Disposed);
|
Assert.True(consumer.Disposed);
|
||||||
Assert.Throws<ObjectDisposedException>(
|
Assert.Throws<ObjectDisposedException>(
|
||||||
() => handler.Subscribe("x", "y"));
|
() => handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = "x" }, "y"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -218,7 +218,7 @@ public sealed class AlarmCommandHandlerTests
|
|||||||
// factory is invoked. We tally invocation counts after each call so
|
// factory is invoked. We tally invocation counts after each call so
|
||||||
// that a missed guard surfaces as the diagnostic count, not a generic
|
// that a missed guard surfaces as the diagnostic count, not a generic
|
||||||
// "Subscribe should have failed".
|
// "Subscribe should have failed".
|
||||||
handler.Subscribe(@"\\HOST\Galaxy!A", "s1");
|
handler.Subscribe(new SubscribeAlarmsCommand { SubscriptionExpression = @"\\HOST\Galaxy!A" }, "s1");
|
||||||
Assert.Equal(1, guardInvocations);
|
Assert.Equal(1, guardInvocations);
|
||||||
|
|
||||||
handler.Acknowledge(Guid.NewGuid(), "c", "u", "n", "d", "F");
|
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.
|
// Subscribe: guard runs before the dispatcher is constructed.
|
||||||
Assert.Throws<InvalidOperationException>(
|
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.
|
// To exercise the other entry points we need a subscribed handler.
|
||||||
// Construct a parallel handler with a passing guard, then swap in a
|
// 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());
|
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)
|
private static MxAlarmSnapshotRecord NewRecord(string provider, string group, string tag)
|
||||||
{
|
{
|
||||||
return new MxAlarmSnapshotRecord
|
return new MxAlarmSnapshotRecord
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ public sealed class MxAccessStaSessionTests
|
|||||||
factory,
|
factory,
|
||||||
eventSink,
|
eventSink,
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
(_eq, _affinity) => handler);
|
(_eq, _affinity, _comFactory) => handler);
|
||||||
|
|
||||||
await session.StartAsync("session-1", workerProcessId: 1);
|
await session.StartAsync("session-1", workerProcessId: 1);
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ public sealed class MxAccessStaSessionTests
|
|||||||
factory,
|
factory,
|
||||||
eventSink,
|
eventSink,
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
(_eq, _affinity) => handler);
|
(_eq, _affinity, _comFactory) => handler);
|
||||||
|
|
||||||
await session.StartAsync("session-1", workerProcessId: 1);
|
await session.StartAsync("session-1", workerProcessId: 1);
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ public sealed class MxAccessStaSessionTests
|
|||||||
factory,
|
factory,
|
||||||
eventSink,
|
eventSink,
|
||||||
new MxAccessEventQueue(),
|
new MxAccessEventQueue(),
|
||||||
(_eq, _affinity) => handler);
|
(_eq, _affinity, _comFactory) => handler);
|
||||||
|
|
||||||
await session.StartAsync("session-1", workerProcessId: 1);
|
await session.StartAsync("session-1", workerProcessId: 1);
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ public sealed class MxAccessStaSessionTests
|
|||||||
factory,
|
factory,
|
||||||
eventSink,
|
eventSink,
|
||||||
eventQueue,
|
eventQueue,
|
||||||
(_eq, _affinity) => handler);
|
(_eq, _affinity, _comFactory) => handler);
|
||||||
|
|
||||||
await session.StartAsync("session-1", workerProcessId: 1);
|
await session.StartAsync("session-1", workerProcessId: 1);
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ public sealed class MxAccessStaSessionTests
|
|||||||
factory,
|
factory,
|
||||||
eventSink,
|
eventSink,
|
||||||
eventQueue,
|
eventQueue,
|
||||||
(_eq, _affinity) => handler);
|
(_eq, _affinity, _comFactory) => handler);
|
||||||
|
|
||||||
await session.StartAsync("session-1", workerProcessId: 1);
|
await session.StartAsync("session-1", workerProcessId: 1);
|
||||||
|
|
||||||
@@ -496,12 +496,12 @@ public sealed class MxAccessStaSessionTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Subscribes to alarm events.</summary>
|
/// <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>
|
/// <param name="sessionId">The session identifier.</param>
|
||||||
public void Subscribe(string subscription, string sessionId)
|
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
|
||||||
{
|
{
|
||||||
IsSubscribed = true;
|
IsSubscribed = true;
|
||||||
LastSubscription = subscription;
|
LastSubscription = command.SubscriptionExpression;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Unsubscribes from alarm events.</summary>
|
/// <summary>Unsubscribes from alarm events.</summary>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class WorkerPipeSession
|
|||||||
options,
|
options,
|
||||||
() => Process.GetCurrentProcess().Id,
|
() => Process.GetCurrentProcess().Id,
|
||||||
new WorkerPipeSessionOptions(),
|
new WorkerPipeSessionOptions(),
|
||||||
() => new MxAccessStaSession((eq, affinity) => new AlarmCommandHandler(eq, () => new WnWrapAlarmConsumer(), affinity)),
|
() => new MxAccessStaSession((eq, affinity, comFactory) => new AlarmCommandHandler(eq, () => new WnWrapAlarmConsumer(), affinity, comFactory, standbyFactory: null)),
|
||||||
logger)
|
logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ public sealed class WorkerPipeSession
|
|||||||
options,
|
options,
|
||||||
processIdProvider,
|
processIdProvider,
|
||||||
new WorkerPipeSessionOptions(),
|
new WorkerPipeSessionOptions(),
|
||||||
() => new MxAccessStaSession((eq, affinity) => new AlarmCommandHandler(eq, () => new WnWrapAlarmConsumer(), affinity)),
|
() => new MxAccessStaSession((eq, affinity, comFactory) => new AlarmCommandHandler(eq, () => new WnWrapAlarmConsumer(), affinity, comFactory, standbyFactory: null)),
|
||||||
logger: null)
|
logger: null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -867,7 +867,7 @@ public sealed class WorkerPipeSession
|
|||||||
// parameterless CompleteStartupHandshakeAsync is used without a
|
// parameterless CompleteStartupHandshakeAsync is used without a
|
||||||
// prior factory call.
|
// prior factory call.
|
||||||
_runtimeSession ??= new MxAccessStaSession(
|
_runtimeSession ??= new MxAccessStaSession(
|
||||||
(eq, affinity) => new AlarmCommandHandler(eq, () => new WnWrapAlarmConsumer(), affinity));
|
(eq, affinity, comFactory) => new AlarmCommandHandler(eq, () => new WnWrapAlarmConsumer(), affinity, comFactory, standbyFactory: null));
|
||||||
IWorkerRuntimeSession session = _runtimeSession;
|
IWorkerRuntimeSession session = _runtimeSession;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,8 +37,14 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
|||||||
private readonly MxAccessEventQueue eventQueue;
|
private readonly MxAccessEventQueue eventQueue;
|
||||||
private readonly Func<IMxAccessAlarmConsumer> consumerFactory;
|
private readonly Func<IMxAccessAlarmConsumer> consumerFactory;
|
||||||
private readonly Action? threadAffinityCheck;
|
private readonly Action? threadAffinityCheck;
|
||||||
|
private readonly IMxAccessComObjectFactory? comFactory;
|
||||||
|
private readonly Func<IReadOnlyList<AlarmSubtagTarget>, IMxAccessAlarmConsumer>? standbyFactory;
|
||||||
|
private readonly MxAccessEventMapper mapper = new MxAccessEventMapper();
|
||||||
private readonly object syncRoot = new object();
|
private readonly object syncRoot = new object();
|
||||||
private AlarmDispatcher? dispatcher;
|
private AlarmDispatcher? dispatcher;
|
||||||
|
private FailoverAlarmConsumer? failoverConsumer;
|
||||||
|
private EventHandler<AlarmProviderModeChange>? providerModeChangedHandler;
|
||||||
|
private string subscribeSessionId = string.Empty;
|
||||||
private bool disposed;
|
private bool disposed;
|
||||||
|
|
||||||
/// <summary>Initializes a new alarm command handler with the given event queue.</summary>
|
/// <summary>Initializes a new alarm command handler with the given event queue.</summary>
|
||||||
@@ -79,10 +85,49 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
|||||||
MxAccessEventQueue eventQueue,
|
MxAccessEventQueue eventQueue,
|
||||||
Func<IMxAccessAlarmConsumer> consumerFactory,
|
Func<IMxAccessAlarmConsumer> consumerFactory,
|
||||||
Action? threadAffinityCheck)
|
Action? threadAffinityCheck)
|
||||||
|
: this(eventQueue, consumerFactory, threadAffinityCheck, comFactory: null, standbyFactory: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full constructor that also threads the MXAccess COM-object factory and
|
||||||
|
/// an optional standby-consumer seam so the subscribe path can build the
|
||||||
|
/// subtag / failover consumers required by
|
||||||
|
/// <see cref="SubscribeAlarmsCommand.ForcedMode"/> and
|
||||||
|
/// <see cref="SubscribeAlarmsCommand.WatchList"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventQueue">The event queue.</param>
|
||||||
|
/// <param name="consumerFactory">
|
||||||
|
/// Factory for the PRIMARY (alarmmgr) consumer — the existing
|
||||||
|
/// wnwrap-backed source. Used alone in alarmmgr mode and as the primary
|
||||||
|
/// of the failover composite in auto mode.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="threadAffinityCheck">Optional STA thread-affinity guard.</param>
|
||||||
|
/// <param name="comFactory">
|
||||||
|
/// The MXAccess COM-object factory used to build the
|
||||||
|
/// <see cref="LmxSubtagAlarmSource"/> backing the subtag consumer. May be
|
||||||
|
/// <see langword="null"/> when a <paramref name="standbyFactory"/> is
|
||||||
|
/// supplied (tests) or when only the alarmmgr path is ever exercised.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="standbyFactory">
|
||||||
|
/// Optional seam that builds the STANDBY (subtag) consumer from a watch
|
||||||
|
/// list. Defaults to a <see cref="SubtagAlarmConsumer"/> over an
|
||||||
|
/// <see cref="LmxSubtagAlarmSource"/> built from
|
||||||
|
/// <paramref name="comFactory"/>. Tests inject a fake so they need no
|
||||||
|
/// live COM factory.
|
||||||
|
/// </param>
|
||||||
|
public AlarmCommandHandler(
|
||||||
|
MxAccessEventQueue eventQueue,
|
||||||
|
Func<IMxAccessAlarmConsumer> consumerFactory,
|
||||||
|
Action? threadAffinityCheck,
|
||||||
|
IMxAccessComObjectFactory? comFactory,
|
||||||
|
Func<IReadOnlyList<AlarmSubtagTarget>, IMxAccessAlarmConsumer>? standbyFactory)
|
||||||
{
|
{
|
||||||
this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
|
this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
|
||||||
this.consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory));
|
this.consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory));
|
||||||
this.threadAffinityCheck = threadAffinityCheck;
|
this.threadAffinityCheck = threadAffinityCheck;
|
||||||
|
this.comFactory = comFactory;
|
||||||
|
this.standbyFactory = standbyFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Gets a value indicating whether the handler is subscribed.</summary>
|
/// <summary>Gets a value indicating whether the handler is subscribed.</summary>
|
||||||
@@ -92,10 +137,10 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Subscribe(string subscription, string sessionId)
|
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
|
||||||
{
|
{
|
||||||
if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
|
if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
|
||||||
if (subscription is null) throw new ArgumentNullException(nameof(subscription));
|
if (command is null) throw new ArgumentNullException(nameof(command));
|
||||||
threadAffinityCheck?.Invoke();
|
threadAffinityCheck?.Invoke();
|
||||||
|
|
||||||
lock (syncRoot)
|
lock (syncRoot)
|
||||||
@@ -106,17 +151,31 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
|||||||
"AlarmCommandHandler already has an active subscription; " +
|
"AlarmCommandHandler already has an active subscription; " +
|
||||||
"call Unsubscribe before issuing another SubscribeAlarms command.");
|
"call Unsubscribe before issuing another SubscribeAlarms command.");
|
||||||
}
|
}
|
||||||
IMxAccessAlarmConsumer consumer = consumerFactory()
|
|
||||||
|
subscribeSessionId = sessionId ?? string.Empty;
|
||||||
|
IMxAccessAlarmConsumer consumer = BuildConsumer(command)
|
||||||
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
|
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
|
||||||
MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(
|
|
||||||
eventQueue, new MxAccessEventMapper());
|
// When the selected consumer is a failover composite, surface its
|
||||||
dispatcher = new AlarmDispatcher(consumer, sink, sessionId ?? string.Empty);
|
// provider switches onto the worker's event queue so connected
|
||||||
|
// gateway clients can observe degraded/recovered state. The handler
|
||||||
|
// is unsubscribed/disposed on Unsubscribe/Dispose below.
|
||||||
|
if (consumer is FailoverAlarmConsumer failover)
|
||||||
|
{
|
||||||
|
failoverConsumer = failover;
|
||||||
|
providerModeChangedHandler = OnProviderModeChanged;
|
||||||
|
failover.ProviderModeChanged += providerModeChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(eventQueue, mapper);
|
||||||
|
dispatcher = new AlarmDispatcher(consumer, sink, subscribeSessionId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
dispatcher.Subscribe(subscription);
|
dispatcher.Subscribe(command.SubscriptionExpression ?? string.Empty);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
DetachProviderModeChanged();
|
||||||
try { dispatcher.Dispose(); } catch { /* swallow */ }
|
try { dispatcher.Dispose(); } catch { /* swallow */ }
|
||||||
dispatcher = null;
|
dispatcher = null;
|
||||||
throw;
|
throw;
|
||||||
@@ -124,6 +183,89 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects and builds the alarm consumer from the command's
|
||||||
|
/// <see cref="SubscribeAlarmsCommand.ForcedMode"/> and
|
||||||
|
/// <see cref="SubscribeAlarmsCommand.WatchList"/>:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>
|
||||||
|
/// <c>Alarmmgr</c>, or <c>Unspecified</c> with an empty watch
|
||||||
|
/// list: the existing primary (alarmmgr) consumer only —
|
||||||
|
/// today's behavior.
|
||||||
|
/// </description></item>
|
||||||
|
/// <item><description>
|
||||||
|
/// <c>Subtag</c>: a <see cref="SubtagAlarmConsumer"/> only.
|
||||||
|
/// </description></item>
|
||||||
|
/// <item><description>
|
||||||
|
/// <c>Unspecified</c> with a non-empty watch list (auto): a
|
||||||
|
/// <see cref="FailoverAlarmConsumer"/> over the primary and a
|
||||||
|
/// subtag standby.
|
||||||
|
/// </description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
private IMxAccessAlarmConsumer BuildConsumer(SubscribeAlarmsCommand command)
|
||||||
|
{
|
||||||
|
List<AlarmSubtagTarget> watchList = new List<AlarmSubtagTarget>(command.WatchList);
|
||||||
|
|
||||||
|
if (command.ForcedMode == AlarmProviderMode.Subtag)
|
||||||
|
{
|
||||||
|
return BuildStandby(watchList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.ForcedMode == AlarmProviderMode.Unspecified && watchList.Count > 0)
|
||||||
|
{
|
||||||
|
IMxAccessAlarmConsumer primary = consumerFactory()
|
||||||
|
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
|
||||||
|
IMxAccessAlarmConsumer standby = BuildStandby(watchList);
|
||||||
|
AlarmFailoverConfig? failoverConfig = command.Failover;
|
||||||
|
FailoverSettings settings = new FailoverSettings(
|
||||||
|
failoverConfig?.ConsecutiveFailureThreshold ?? 3,
|
||||||
|
failoverConfig?.FailbackProbeIntervalSeconds ?? 30,
|
||||||
|
failoverConfig?.FailbackStableProbes ?? 3);
|
||||||
|
return new FailoverAlarmConsumer(primary, standby, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alarmmgr, or Unspecified with an empty watch list — primary only.
|
||||||
|
return consumerFactory()
|
||||||
|
?? throw new InvalidOperationException("Alarm consumer factory returned null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private IMxAccessAlarmConsumer BuildStandby(IReadOnlyList<AlarmSubtagTarget> watchList)
|
||||||
|
{
|
||||||
|
if (standbyFactory is not null)
|
||||||
|
{
|
||||||
|
return standbyFactory(watchList)
|
||||||
|
?? throw new InvalidOperationException("Standby alarm consumer factory returned null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comFactory is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Subtag alarm consumer requires an IMxAccessComObjectFactory; the alarm command "
|
||||||
|
+ "handler was constructed without one and no standby factory was supplied.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SubtagAlarmConsumer(new LmxSubtagAlarmSource(comFactory), watchList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnProviderModeChanged(object? sender, AlarmProviderModeChange change)
|
||||||
|
{
|
||||||
|
if (change is null) return;
|
||||||
|
eventQueue.Enqueue(mapper.CreateOnAlarmProviderModeChanged(
|
||||||
|
subscribeSessionId, change.Mode, change.Reason, change.HResult, change.AtUtc));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetachProviderModeChanged()
|
||||||
|
{
|
||||||
|
if (failoverConsumer is not null && providerModeChangedHandler is not null)
|
||||||
|
{
|
||||||
|
try { failoverConsumer.ProviderModeChanged -= providerModeChangedHandler; }
|
||||||
|
catch { /* swallow */ }
|
||||||
|
}
|
||||||
|
failoverConsumer = null;
|
||||||
|
providerModeChangedHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Unsubscribe()
|
public void Unsubscribe()
|
||||||
{
|
{
|
||||||
@@ -131,6 +273,7 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
|
|||||||
AlarmDispatcher? toDispose;
|
AlarmDispatcher? toDispose;
|
||||||
lock (syncRoot)
|
lock (syncRoot)
|
||||||
{
|
{
|
||||||
|
DetachProviderModeChanged();
|
||||||
toDispose = dispatcher;
|
toDispose = dispatcher;
|
||||||
dispatcher = null;
|
dispatcher = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,16 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IAlarmCommandHandler : IDisposable
|
public interface IAlarmCommandHandler : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>Begin a subscription against the supplied AVEVA alarm-provider expression.</summary>
|
/// <summary>
|
||||||
/// <param name="subscription">The AVEVA alarm-provider subscription expression.</param>
|
/// Begin an alarm subscription from the supplied command. The command's
|
||||||
|
/// <see cref="SubscribeAlarmsCommand.ForcedMode"/> and
|
||||||
|
/// <see cref="SubscribeAlarmsCommand.WatchList"/> select the consumer:
|
||||||
|
/// alarmmgr-only (the default), subtag-only, or an auto-failover
|
||||||
|
/// composite over both.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">The full SubscribeAlarms command.</param>
|
||||||
/// <param name="sessionId">The session identifier.</param>
|
/// <param name="sessionId">The session identifier.</param>
|
||||||
void Subscribe(string subscription, string sessionId);
|
void Subscribe(SubscribeAlarmsCommand command, string sessionId);
|
||||||
|
|
||||||
/// <summary>Tear down the active subscription. No-op if not subscribed.</summary>
|
/// <summary>Tear down the active subscription. No-op if not subscribed.</summary>
|
||||||
void Unsubscribe();
|
void Unsubscribe();
|
||||||
|
|||||||
@@ -598,7 +598,8 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
|||||||
"SubscribeAlarms requires an alarm command handler; the worker was constructed without one.");
|
"SubscribeAlarms requires an alarm command handler; the worker was constructed without one.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string subscription = command.Command.SubscribeAlarms.SubscriptionExpression ?? string.Empty;
|
SubscribeAlarmsCommand subscribeCommand = command.Command.SubscribeAlarms;
|
||||||
|
string subscription = subscribeCommand.SubscriptionExpression ?? string.Empty;
|
||||||
if (string.IsNullOrWhiteSpace(subscription))
|
if (string.IsNullOrWhiteSpace(subscription))
|
||||||
{
|
{
|
||||||
return CreateInvalidRequestReply(command, "SubscribeAlarms.subscription_expression is required.");
|
return CreateInvalidRequestReply(command, "SubscribeAlarms.subscription_expression is required.");
|
||||||
@@ -606,7 +607,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
alarmCommandHandler.Subscribe(subscription, command.SessionId);
|
alarmCommandHandler.Subscribe(subscribeCommand, command.SessionId);
|
||||||
return CreateOkReply(command);
|
return CreateOkReply(command);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -182,6 +182,43 @@ public sealed class MxAccessEventMapper
|
|||||||
return mxEvent;
|
return mxEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an OnAlarmProviderModeChanged event from a
|
||||||
|
/// <see cref="FailoverAlarmConsumer"/> provider switch. The worker's
|
||||||
|
/// alarm path drives this when the failover composite switches between
|
||||||
|
/// the alarmmgr primary and the subtag standby, so connected gateway
|
||||||
|
/// clients can observe the degraded/recovered provider state.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionId">Identifier of the session.</param>
|
||||||
|
/// <param name="mode">The provider mode now active after the switch.</param>
|
||||||
|
/// <param name="reason">Human-readable reason for the switch.</param>
|
||||||
|
/// <param name="hresult">The COM HRESULT that triggered a failover, or 0 for a clean failback.</param>
|
||||||
|
/// <param name="atUtc">The UTC instant the switch occurred.</param>
|
||||||
|
public MxEvent CreateOnAlarmProviderModeChanged(
|
||||||
|
string sessionId,
|
||||||
|
AlarmProviderMode mode,
|
||||||
|
string reason,
|
||||||
|
int hresult,
|
||||||
|
DateTime atUtc)
|
||||||
|
{
|
||||||
|
MxEvent mxEvent = CreateBaseEvent(
|
||||||
|
MxEventFamily.OnAlarmProviderModeChanged,
|
||||||
|
sessionId,
|
||||||
|
serverHandle: 0,
|
||||||
|
itemHandle: 0,
|
||||||
|
statuses: null);
|
||||||
|
|
||||||
|
mxEvent.OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
|
||||||
|
{
|
||||||
|
Mode = mode,
|
||||||
|
Reason = reason ?? string.Empty,
|
||||||
|
Hresult = hresult,
|
||||||
|
At = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
||||||
|
DateTime.SpecifyKind(atUtc, DateTimeKind.Utc)),
|
||||||
|
};
|
||||||
|
return mxEvent;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Creates an OnBufferedDataChange event from MXAccess COM event arguments.</summary>
|
/// <summary>Creates an OnBufferedDataChange event from MXAccess COM event arguments.</summary>
|
||||||
/// <param name="sessionId">Identifier of the session.</param>
|
/// <param name="sessionId">Identifier of the session.</param>
|
||||||
/// <param name="serverHandle">Handle returned by the worker.</param>
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
|
|||||||
// then invokes the guard at the entry of every method that touches the
|
// then invokes the guard at the entry of every method that touches the
|
||||||
// wnwrap consumer, matching the STA-affinity invariant already enforced
|
// wnwrap consumer, matching the STA-affinity invariant already enforced
|
||||||
// for the poll path via EnsureOnAlarmConsumerThread.
|
// for the poll path via EnsureOnAlarmConsumerThread.
|
||||||
private readonly Func<MxAccessEventQueue, Action, IAlarmCommandHandler>? alarmCommandHandlerFactory;
|
// Worker-9: the third arg is the session's IMxAccessComObjectFactory, so
|
||||||
|
// the handler can build the subtag-fallback source's own proxy-server COM
|
||||||
|
// object on this STA when a subscribe selects the subtag / failover path.
|
||||||
|
private readonly Func<MxAccessEventQueue, Action, IMxAccessComObjectFactory, IAlarmCommandHandler>? alarmCommandHandlerFactory;
|
||||||
private StaCommandDispatcher? commandDispatcher;
|
private StaCommandDispatcher? commandDispatcher;
|
||||||
private MxAccessSession? session;
|
private MxAccessSession? session;
|
||||||
private IAlarmCommandHandler? alarmCommandHandler;
|
private IAlarmCommandHandler? alarmCommandHandler;
|
||||||
@@ -51,7 +54,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
|
|||||||
/// of alarm-side commands.
|
/// of alarm-side commands.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="alarmCommandHandlerFactory">Factory that constructs the alarm-command handler.</param>
|
/// <param name="alarmCommandHandlerFactory">Factory that constructs the alarm-command handler.</param>
|
||||||
internal MxAccessStaSession(Func<MxAccessEventQueue, Action, IAlarmCommandHandler>? alarmCommandHandlerFactory)
|
internal MxAccessStaSession(Func<MxAccessEventQueue, Action, IMxAccessComObjectFactory, IAlarmCommandHandler>? alarmCommandHandlerFactory)
|
||||||
: this(
|
: this(
|
||||||
new StaRuntime(),
|
new StaRuntime(),
|
||||||
new MxAccessComObjectFactory(),
|
new MxAccessComObjectFactory(),
|
||||||
@@ -103,7 +106,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
|
|||||||
StaRuntime staRuntime,
|
StaRuntime staRuntime,
|
||||||
IMxAccessComObjectFactory factory,
|
IMxAccessComObjectFactory factory,
|
||||||
MxAccessEventQueue eventQueue,
|
MxAccessEventQueue eventQueue,
|
||||||
Func<MxAccessEventQueue, Action, IAlarmCommandHandler>? alarmCommandHandlerFactory)
|
Func<MxAccessEventQueue, Action, IMxAccessComObjectFactory, IAlarmCommandHandler>? alarmCommandHandlerFactory)
|
||||||
: this(staRuntime, factory, new MxAccessBaseEventSink(eventQueue), eventQueue, alarmCommandHandlerFactory)
|
: this(staRuntime, factory, new MxAccessBaseEventSink(eventQueue), eventQueue, alarmCommandHandlerFactory)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -141,7 +144,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
|
|||||||
IMxAccessComObjectFactory factory,
|
IMxAccessComObjectFactory factory,
|
||||||
IMxAccessEventSink eventSink,
|
IMxAccessEventSink eventSink,
|
||||||
MxAccessEventQueue eventQueue,
|
MxAccessEventQueue eventQueue,
|
||||||
Func<MxAccessEventQueue, Action, IAlarmCommandHandler>? alarmCommandHandlerFactory)
|
Func<MxAccessEventQueue, Action, IMxAccessComObjectFactory, IAlarmCommandHandler>? alarmCommandHandlerFactory)
|
||||||
{
|
{
|
||||||
this.staRuntime = staRuntime ?? throw new ArgumentNullException(nameof(staRuntime));
|
this.staRuntime = staRuntime ?? throw new ArgumentNullException(nameof(staRuntime));
|
||||||
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||||
@@ -209,9 +212,16 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
|
|||||||
// on convention alone; a future refactor that let a
|
// on convention alone; a future refactor that let a
|
||||||
// command run off-STA would silently deadlock on
|
// command run off-STA would silently deadlock on
|
||||||
// cross-apartment marshaling against the wnwrap consumer.
|
// cross-apartment marshaling against the wnwrap consumer.
|
||||||
|
// Worker-9: the factory also receives the session's
|
||||||
|
// IMxAccessComObjectFactory so the subtag-fallback source
|
||||||
|
// (LmxSubtagAlarmSource) can create its OWN proxy-server COM
|
||||||
|
// object on this STA, isolated from the item pipeline's
|
||||||
|
// MxAccessSession. The factory call runs on the STA, so the
|
||||||
|
// resulting source is bound to the correct apartment.
|
||||||
alarmCommandHandler = alarmCommandHandlerFactory(
|
alarmCommandHandler = alarmCommandHandlerFactory(
|
||||||
eventQueue,
|
eventQueue,
|
||||||
EnsureOnAlarmConsumerThread);
|
EnsureOnAlarmConsumerThread,
|
||||||
|
factory);
|
||||||
}
|
}
|
||||||
commandDispatcher = new StaCommandDispatcher(
|
commandDispatcher = new StaCommandDispatcher(
|
||||||
staRuntime,
|
staRuntime,
|
||||||
|
|||||||
Reference in New Issue
Block a user