test: expose concurrent SDK2 host disposal completion race
This commit is contained in:
+99
@@ -0,0 +1,99 @@
|
|||||||
|
using Opc.Ua;
|
||||||
|
using Opc.Ua.Configuration;
|
||||||
|
using Opc.Ua.Server;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ClientSession = Opc.Ua.Client.ISession;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
|
||||||
|
|
||||||
|
public sealed partial class Sdk2LifecycleSoakTests
|
||||||
|
{
|
||||||
|
/// <summary>Concurrent host disposal awaits real encrypted-session shutdown before clearing ownership.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Concurrent_host_disposal_waits_for_the_actual_encrypted_session_to_close()
|
||||||
|
{
|
||||||
|
var ct = TestContext.Current.CancellationToken;
|
||||||
|
var clientRoot = Path.Combine(Path.GetTempPath(), $"otua-dispose-client-{Guid.NewGuid():N}");
|
||||||
|
ApplicationInstance? application = null;
|
||||||
|
ServerEventRig? rig = null;
|
||||||
|
ClientSession? client = null;
|
||||||
|
ISessionManager? sessionManager = null;
|
||||||
|
SessionEventHandler? closing = null;
|
||||||
|
using var release = new ManualResetEventSlim(false);
|
||||||
|
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var calls = new List<Task>();
|
||||||
|
var timedOut = 0;
|
||||||
|
var callbackCount = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var telemetry = DefaultTelemetry.Create(_ => { });
|
||||||
|
var security = TestClientSecurity.Build(clientRoot);
|
||||||
|
security.ApplicationCertificate!.SubjectName = "CN=SDK2LifecycleSoak";
|
||||||
|
var config = new ApplicationConfiguration(telemetry)
|
||||||
|
{
|
||||||
|
ApplicationName = "SDK2LifecycleSoak", ApplicationUri = $"urn:SDK2LifecycleSoak:{Guid.NewGuid():N}",
|
||||||
|
ApplicationType = ApplicationType.Client, SecurityConfiguration = security,
|
||||||
|
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15_000 },
|
||||||
|
TransportQuotas = new TransportQuotas { OperationTimeout = 5000 },
|
||||||
|
};
|
||||||
|
application = new ApplicationInstance(telemetry) { ApplicationConfiguration = config };
|
||||||
|
await config.ValidateAsync(ApplicationType.Client, ct);
|
||||||
|
await application.CheckApplicationInstanceCertificatesAsync(true, null, ct);
|
||||||
|
rig = await ServerEventRig.StartAsync("SDK2ConcurrentDisposal", encrypted: true, ct);
|
||||||
|
Configure(rig);
|
||||||
|
client = await ConnectAsync(rig, config, null, ct);
|
||||||
|
sessionManager = rig.Server.CurrentInstance.SessionManager;
|
||||||
|
sessionManager.GetSessions().Count.ShouldBe(1);
|
||||||
|
var sessionId = client.SessionId;
|
||||||
|
closing = (session, reason) =>
|
||||||
|
{
|
||||||
|
if (session.Id != sessionId || reason != SessionEventReason.Closing) return;
|
||||||
|
Interlocked.Increment(ref callbackCount);
|
||||||
|
entered.TrySetResult();
|
||||||
|
// SDK swallows event exceptions; keep an explicit failure flag for this bounded barrier.
|
||||||
|
if (!release.Wait(TimeSpan.FromSeconds(15))) Interlocked.Exchange(ref timedOut, 1);
|
||||||
|
};
|
||||||
|
sessionManager.SessionClosing += closing;
|
||||||
|
var host = RequiredField<OpcUaApplicationHost>(rig, "_host");
|
||||||
|
var manager = rig.Server.CertificateManager!;
|
||||||
|
calls.Add(Task.Run(async () => await host.DisposeAsync(), ct));
|
||||||
|
await entered.Task.WaitAsync(TimeSpan.FromSeconds(10), ct);
|
||||||
|
for (var i = 0; i < 3; i++) calls.Add(host.DisposeAsync().AsTask());
|
||||||
|
calls.ShouldAllBe(call => !call.IsCompleted, "every caller must await the actual shutdown owner");
|
||||||
|
host.Server.ShouldBeSameAs(rig.Server);
|
||||||
|
RequiredField<bool>(manager, "m_disposed").ShouldBeFalse();
|
||||||
|
release.Set();
|
||||||
|
await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), ct);
|
||||||
|
Volatile.Read(ref timedOut).ShouldBe(0);
|
||||||
|
Volatile.Read(ref callbackCount).ShouldBe(1);
|
||||||
|
host.Server.ShouldBeNull();
|
||||||
|
host.ApplicationInstance.ShouldBeNull();
|
||||||
|
RequiredField<bool>(manager, "m_disposed").ShouldBeTrue();
|
||||||
|
CertificateCores(manager).Sum(CachedStores).ShouldBe(0);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Removing a handler takes the SDK's event lock: release before awaiting or unsubscribing.
|
||||||
|
release.Set();
|
||||||
|
try { await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (sessionManager is not null && closing is not null) sessionManager.SessionClosing -= closing; }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { client?.Dispose(); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (rig is not null) await rig.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (application is not null) await application.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
|
||||||
|
finally { if (Directory.Exists(clientRoot)) Directory.Delete(clientRoot, true); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user