From 2997cb8a2cec000bd47c6063e6ca76edc8b2fa04 Mon Sep 17 00:00:00 2001 From: romanett Date: Mon, 19 Oct 2026 19:21:20 +0200 Subject: [PATCH] SessionBindingTests: let readers confirm the live session before racing transfers (#4848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `BindingLivenessRacingTransfersAndCloseNeverConfirmsReplacedBindingAsync` failed on the net48 CI runner (test-windows-net48 11/16, run 39250649914, on docs-only PR #4841) with `confirmed = 0`. The safety check (`errors`) was empty. The reader tasks were not scheduled until all 1000 transfers and the close had finished, so the liveness check never saw a live binding. ## Fix (test only) Before the first transfer, the test now waits until every reader has confirmed the live binding at least once (bounded: 30 s, then a clear `Assert.Fail`). The race still covers transfers and close, but the positive assertion no longer depends on thread-pool scheduling. The safety assertion is unchanged. ## Verification I ran the test with a temporary `[Repeat(100)]` in Release builds: - net10.0 and net48: 100 repeats each, no load - net10.0 and net48: 3 × 100 repeats each with 2 busy loops per core 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- .../SessionBindingTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Opc.Ua.Server.Tests/SessionBindingTests.cs b/tests/Opc.Ua.Server.Tests/SessionBindingTests.cs index f51a08f563..960cce8741 100644 --- a/tests/Opc.Ua.Server.Tests/SessionBindingTests.cs +++ b/tests/Opc.Ua.Server.Tests/SessionBindingTests.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Time.Testing; @@ -619,6 +620,7 @@ namespace Opc.Ua.Server.Tests int closed = 0; int checks = 0; int confirmed = 0; + int readersLive = 0; var errors = new ConcurrentQueue(); using var stop = new CancellationTokenSource(); @@ -629,6 +631,7 @@ namespace Opc.Ua.Server.Tests readers[r] = Task.Run(() => { int attempt = 0; + bool seenLive = false; while (!stop.IsCancellationRequested) { OperationContext channel = channels[(reader + attempt++) & 1]; @@ -650,6 +653,11 @@ namespace Opc.Ua.Server.Tests break; } Interlocked.Increment(ref confirmed); + if (!seenLive) + { + seenLive = true; + Interlocked.Increment(ref readersLive); + } if (closedBefore || binding.ActivationSequence < floor) { errors.Enqueue( @@ -662,6 +670,18 @@ namespace Opc.Ua.Server.Tests try { + // let every reader confirm the live session once before the race starts, so the + // positive check does not depend on the pool scheduling readers during transfers + var deadline = Stopwatch.StartNew(); + while (Volatile.Read(ref readersLive) < kReaders && errors.IsEmpty) + { + if (deadline.Elapsed > TimeSpan.FromSeconds(30)) + { + Assert.Fail( + $"Only {Volatile.Read(ref readersLive)} of {kReaders} readers confirmed the live session."); + } + await Task.Delay(1).ConfigureAwait(false); + } for (int i = 1; i <= kTransfers; i++) { await ActivateAsync(created, channels[i & 1]).ConfigureAwait(false);