Fix MetricsAreEmittedForReconnectAndGateWaitAsync race on fast runners

CI failure (AzDO build 14553 log 874, net48 Windows): the test asserted
that opcua.channel.reconnect.duration was emitted but only saw the
attempts / gate.wait / open / active measurements.

Root cause: race in the TEST (production behavior is correct).
ChannelEntry.RunReconnectCycleAsync records RecordReconnectAttempt
BEFORE calling tcs.TrySetResult(true), but RecordReconnectDuration
is invoked in the finally block AFTER TrySetResult returns control.
The test awaits the reconnect task — which completes on TrySetResult
— and then immediately asserts both metrics. On a fast net48 runner
the test thread reaches the duration assertion before the finally
block runs, so the histogram measurement is missing.

Add a WaitForMeasurementAsync helper that polls for the duration
measurement with a 2-second budget before the existing hard assertion
fires. The hard assertion is preserved so the test still fails if
the metric is genuinely never emitted.

Verified locally: net48 5/5 runs pass (was 0/1 in CI), net10 1/1 (no
regression).
This commit is contained in:
Marc Schier
2026-06-10 08:52:41 +02:00
parent 51e7f42bd7
commit 53ff029df8
@@ -773,6 +773,17 @@ namespace Opc.Ua.Client.Tests.Stack.Client
await Task.WhenAll(reconnectTask, sendTask).ConfigureAwait(false);
// RunReconnectCycleAsync.RecordReconnectDuration is emitted from a
// finally block AFTER tcs.TrySetResult(true) returns, so on a fast
// runner the test thread can race past the assertion before the
// measurement lands. Poll briefly for the histogram measurement.
await WaitForMeasurementAsync(
metrics,
"opcua.channel.reconnect.duration",
Tag("endpoint", endpointUrl),
Tag("outcome", "success"))
.ConfigureAwait(false);
Assert.That(metrics.HasMeasurement(
"opcua.channel.reconnect.attempts",
Tag("endpoint", endpointUrl),
@@ -793,6 +804,21 @@ namespace Opc.Ua.Client.Tests.Stack.Client
}
}
private static async Task WaitForMeasurementAsync(
ChannelMetricListener metrics,
string instrumentName,
params KeyValuePair<string, object?>[] tags)
{
const int kMaxPollMs = 2000;
const int kPollIntervalMs = 25;
int elapsed = 0;
while (!metrics.HasMeasurement(instrumentName, tags) && elapsed < kMaxPollMs)
{
await Task.Delay(kPollIntervalMs).ConfigureAwait(false);
elapsed += kPollIntervalMs;
}
}
[Test]
[NonParallelizable]
public async Task ActivitySpanIsRecordedForReconnectAsync()