SessionBindingTests: let readers confirm the live session before racing transfers (#4848)

## 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 <noreply@anthropic.com>
This commit is contained in:
romanett
2026-10-19 19:21:20 +02:00
committed by GitHub
parent a6cc7c4bc9
commit 2997cb8a2c
@@ -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<string>();
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);