05a0e51fc1
The SDK's DisposeAsync closes with no token, i.e. under the 30 s DefaultCallTimeout, so a blackholed gateway parked the driver's teardown for the whole of it. CloseAsync(ct) runs the close under the caller's token, drops the channel on a cancelled/failed close (the gateway reaps the session on its idle lease) and leaves the session re-openable. A close followed by the final dispose closes the gateway session once. Refs #735 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
91 lines
3.9 KiB
C#
91 lines
3.9 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
|
|
|
/// <summary>
|
|
/// <see cref="GalaxyMxSession.CloseAsync"/> bounds the gateway <c>CloseSession</c> round-trip by
|
|
/// the caller's token (the SDK dispose closes under its 30 s call timeout) and leaves the session
|
|
/// re-openable.
|
|
/// </summary>
|
|
public sealed class GalaxyMxSessionCloseTests
|
|
{
|
|
/// <summary>A close against a never-answering gateway returns when the token fires, without throwing.</summary>
|
|
[Fact]
|
|
public async Task CloseAsync_returns_at_the_token_deadline_against_a_never_answering_gateway()
|
|
{
|
|
var session = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest"))
|
|
{
|
|
OpenAndRegisterOverrideForTests = _ => Task.CompletedTask,
|
|
CloseOverrideForTests = ct => Task.Delay(Timeout.Infinite, ct),
|
|
};
|
|
await session.ConnectAsync(null!, CancellationToken.None);
|
|
session.IsConnected.ShouldBeTrue();
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
await session.CloseAsync(cts.Token).WaitAsync(TimeSpan.FromSeconds(10));
|
|
sw.Stop();
|
|
|
|
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(5));
|
|
session.IsConnected.ShouldBeFalse();
|
|
|
|
// Still usable: a close is not a dispose.
|
|
await session.ConnectAsync(null!, CancellationToken.None);
|
|
session.IsConnected.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>Positive control: a close that answers completes and reports the token it was handed.</summary>
|
|
[Fact]
|
|
public async Task CloseAsync_completes_when_the_gateway_answers()
|
|
{
|
|
CancellationToken seen = default;
|
|
var session = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest"))
|
|
{
|
|
OpenAndRegisterOverrideForTests = _ => Task.CompletedTask,
|
|
CloseOverrideForTests = ct => { seen = ct; return Task.CompletedTask; },
|
|
};
|
|
await session.ConnectAsync(null!, CancellationToken.None);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
await session.CloseAsync(cts.Token);
|
|
|
|
seen.ShouldBe(cts.Token, "the caller's token must reach the gateway round-trip");
|
|
session.IsConnected.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The driver's teardown is CloseAsync (bounded) then DisposeAsync (final). The dispose must
|
|
/// not issue a second gateway close; a fresh connect after a close must close again (positive
|
|
/// control: the guard keys on "was connected", not on "ever closed").
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task CloseAsync_then_DisposeAsync_closes_the_gateway_session_once()
|
|
{
|
|
var closes = 0;
|
|
var session = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest"))
|
|
{
|
|
OpenAndRegisterOverrideForTests = _ => Task.CompletedTask,
|
|
CloseOverrideForTests = _ => { Interlocked.Increment(ref closes); return Task.CompletedTask; },
|
|
};
|
|
await session.ConnectAsync(null!, CancellationToken.None);
|
|
|
|
await session.CloseAsync(CancellationToken.None);
|
|
await session.DisposeAsync();
|
|
closes.ShouldBe(1, "dispose after a close must not close the gateway session again");
|
|
|
|
var reopened = new GalaxyMxSession(new GalaxyMxAccessOptions("CloseTest"))
|
|
{
|
|
OpenAndRegisterOverrideForTests = _ => Task.CompletedTask,
|
|
CloseOverrideForTests = _ => { Interlocked.Increment(ref closes); return Task.CompletedTask; },
|
|
};
|
|
await reopened.ConnectAsync(null!, CancellationToken.None);
|
|
await reopened.CloseAsync(CancellationToken.None);
|
|
await reopened.ConnectAsync(null!, CancellationToken.None);
|
|
await reopened.DisposeAsync();
|
|
closes.ShouldBe(3, "each live session closes exactly once");
|
|
}
|
|
}
|