Keep subtag acks and alarm reads safe on the managed ack path
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (pull_request) Successful in 2m0s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m16s
ci / java (push) Successful in 2m19s
ci / portable (push) Failing after 2m31s
ci / portable (pull_request) Failing after 2m36s
ci / windows-x86 (push) Successful in 3m11s
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (pull_request) Successful in 2m0s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m16s
ci / java (push) Successful in 2m19s
ci / portable (push) Failing after 2m31s
ci / portable (pull_request) Failing after 2m36s
ci / windows-x86 (push) Successful in 3m11s
Review fixes for the opt-in managed GUID ack: - Subtag-mode GUID acks (forced subtag, or failover on the standby) stay on the consumer; their GUIDs are synthetic and only the subtag consumer can resolve them. Forced-subtag sessions no longer build a managed client. - A failed managed Initialize/Register/Subscribe drops back to the wnwrap ack path instead of failing the alarm subscription. - Drop stale crefs to a nonexistent Unverified member in WnalNativeAckConsumer. - Unit tests for routing, fallback, and disposal; document MXGATEWAY_USE_MANAGED_ALARM_ACK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -203,6 +203,100 @@ public sealed class AlarmDispatcherTests
|
||||
Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a managed ack client takes GUID acks with full operator identity.</summary>
|
||||
[Fact]
|
||||
public void Acknowledge_WithManagedAckClient_RoutesGuidAckToManagedClient()
|
||||
{
|
||||
FakeAlarmConsumer consumer = new FakeAlarmConsumer { AcknowledgeReturn = -1 };
|
||||
FakeManagedAckClient managed = new FakeManagedAckClient { AcknowledgeReturn = 0 };
|
||||
using AlarmDispatcher dispatcher = new AlarmDispatcher(
|
||||
consumer,
|
||||
new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()),
|
||||
SessionId,
|
||||
managed);
|
||||
dispatcher.Subscribe(@"\\HOST\Galaxy!Area1");
|
||||
|
||||
Guid guid = Guid.NewGuid();
|
||||
int rc = dispatcher.Acknowledge(guid, "Acked", "alice", "WS01", "CORP", "Alice Smith");
|
||||
|
||||
Assert.Equal(0, rc);
|
||||
Assert.Equal(@"\\HOST\Galaxy!Area1", managed.LastSubscription);
|
||||
Assert.Equal(guid, managed.LastAckGuid);
|
||||
Assert.Equal("CORP", managed.LastAckOperatorDomain);
|
||||
Assert.Equal("Alice Smith", managed.LastAckOperatorFullName);
|
||||
Assert.Equal(Guid.Empty, consumer.LastAckGuid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a failed managed-client lifecycle neither fails the alarm
|
||||
/// subscription nor leaves acks routed to the broken client.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Subscribe_WhenManagedAckClientFails_FallsBackToConsumerAck()
|
||||
{
|
||||
FakeAlarmConsumer consumer = new FakeAlarmConsumer { AcknowledgeReturn = 0 };
|
||||
FakeManagedAckClient managed = new FakeManagedAckClient { ThrowOnSubscribe = true };
|
||||
using AlarmDispatcher dispatcher = new AlarmDispatcher(
|
||||
consumer,
|
||||
new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()),
|
||||
SessionId,
|
||||
managed);
|
||||
|
||||
dispatcher.Subscribe(@"\\HOST\Galaxy!Area1");
|
||||
Guid guid = Guid.NewGuid();
|
||||
int rc = dispatcher.Acknowledge(guid, "Acked", "alice", "WS01", "CORP", "Alice Smith");
|
||||
|
||||
Assert.Equal(@"\\HOST\Galaxy!Area1", consumer.LastSubscription);
|
||||
Assert.True(managed.Disposed);
|
||||
Assert.Equal(0, rc);
|
||||
Assert.Equal(guid, consumer.LastAckGuid);
|
||||
Assert.Equal(Guid.Empty, managed.LastAckGuid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that while the failover consumer serves the subtag standby,
|
||||
/// GUID acks (synthetic GUIDs) stay on the consumer, not the managed client.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Acknowledge_WhenFailoverInSubtagMode_KeepsGuidAckOnConsumer()
|
||||
{
|
||||
FakeAlarmConsumer primary = new FakeAlarmConsumer { ThrowOnSubscribe = true };
|
||||
FakeAlarmConsumer standby = new FakeAlarmConsumer { AcknowledgeReturn = 0 };
|
||||
FailoverAlarmConsumer failover = new FailoverAlarmConsumer(
|
||||
primary, standby, new FailoverSettings(threshold: 1, probeIntervalSeconds: 0, stableProbes: 3));
|
||||
FakeManagedAckClient managed = new FakeManagedAckClient { AcknowledgeReturn = -1 };
|
||||
using AlarmDispatcher dispatcher = new AlarmDispatcher(
|
||||
failover,
|
||||
new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()),
|
||||
SessionId,
|
||||
managed);
|
||||
dispatcher.Subscribe(@"\\HOST\Galaxy!Area1");
|
||||
Assert.Equal(AlarmProviderMode.Subtag, failover.Mode);
|
||||
|
||||
Guid guid = Guid.NewGuid();
|
||||
int rc = dispatcher.Acknowledge(guid, "Acked", "alice", "WS01", "CORP", "Alice Smith");
|
||||
|
||||
Assert.Equal(0, rc);
|
||||
Assert.Equal(guid, standby.LastAckGuid);
|
||||
Assert.Equal(Guid.Empty, managed.LastAckGuid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the dispatcher disposes the managed ack client.</summary>
|
||||
[Fact]
|
||||
public void Dispose_WithManagedAckClient_DisposesManagedClient()
|
||||
{
|
||||
FakeManagedAckClient managed = new FakeManagedAckClient();
|
||||
AlarmDispatcher dispatcher = new AlarmDispatcher(
|
||||
new FakeAlarmConsumer(),
|
||||
new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()),
|
||||
SessionId,
|
||||
managed);
|
||||
|
||||
dispatcher.Dispose();
|
||||
|
||||
Assert.True(managed.Disposed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that acknowledge-by-name calls are forwarded to the consumer.</summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByName_WhenInvoked_ForwardsToConsumerWithFullTuple()
|
||||
@@ -470,9 +564,13 @@ public sealed class AlarmDispatcherTests
|
||||
AlarmTransitionEmitted?.Invoke(this, transition);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether <see cref="Subscribe"/> throws.</summary>
|
||||
public bool ThrowOnSubscribe { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Subscribe(string subscription)
|
||||
{
|
||||
if (ThrowOnSubscribe) throw new InvalidOperationException("subscribe failed");
|
||||
LastSubscription = subscription;
|
||||
}
|
||||
|
||||
@@ -533,4 +631,50 @@ public sealed class AlarmDispatcherTests
|
||||
Disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeManagedAckClient : IManagedAlarmAckClient
|
||||
{
|
||||
/// <summary>Gets the last subscription expression.</summary>
|
||||
public string? LastSubscription { get; private set; }
|
||||
/// <summary>Gets the GUID from the last acknowledge call.</summary>
|
||||
public Guid LastAckGuid { get; private set; }
|
||||
/// <summary>Gets the operator domain from the last acknowledge call.</summary>
|
||||
public string? LastAckOperatorDomain { get; private set; }
|
||||
/// <summary>Gets the operator full name from the last acknowledge call.</summary>
|
||||
public string? LastAckOperatorFullName { get; private set; }
|
||||
/// <summary>Gets or sets the return code for acknowledge operations.</summary>
|
||||
public int AcknowledgeReturn { get; set; }
|
||||
/// <summary>Gets or sets a value indicating whether <see cref="Subscribe"/> throws.</summary>
|
||||
public bool ThrowOnSubscribe { get; set; }
|
||||
/// <summary>Gets a value indicating whether this instance has been disposed.</summary>
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Subscribe(string subscription)
|
||||
{
|
||||
if (ThrowOnSubscribe) throw new InvalidOperationException("RegisterConsumer returned 5.");
|
||||
LastSubscription = subscription;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AcknowledgeByGuid(
|
||||
Guid alarmGuid,
|
||||
string comment,
|
||||
string operatorName,
|
||||
string operatorNode,
|
||||
string operatorDomain,
|
||||
string operatorFullName)
|
||||
{
|
||||
LastAckGuid = alarmGuid;
|
||||
LastAckOperatorDomain = operatorDomain;
|
||||
LastAckOperatorFullName = operatorFullName;
|
||||
return AcknowledgeReturn;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user