Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATCapturingLogger.cs
T
Joseph Doherty f0992394b6 fix(twincat): book a refused registration's backoff before releasing ConnectGate (lmxopcua#773)
RegisterNotificationAsync published a refused registration into
NativeRegistrations under ConnectGate with NextRetryUtc = MinValue; the
SubscribeAsync caller booked the refusal backoff (NoteDeadFailure refused:
true) only after the gate was released and after its WARN. A probe tick in
that window took the gate, saw the registration as due, and retried it at
once - the CI flake in A_rearm_during_an_in_flight_retry_is_not_lost
("AttemptsFor(\"MAIN.T\") should be 1 but was 2").

RegisterNotificationAsync now books the failure (refusal backoff, or the
comms-class every-tick retry) before it publishes the dead registration, still
under the gate. The caller no longer books the refusal a second time (that
would double the backoff); its comms-class booking is idempotent and stays.

Tests: TwinCATCapturingLogger gains an OnWarning hook. A logger that holds
the refusal WARN for ten probe intervals forces the window open, so
A_rearm_during_an_in_flight_retry_is_not_lost exercises it on every run, and
the new A_refused_registration_is_published_with_its_backoff_already_set
pins the backoff (booked once, not doubled).

Pass rate of A_rearm_during_an_in_flight_retry_is_not_lost:
- before, unmodified test: 40/40 runs, plus 200/200 in-process repeats under
  CPU load (the natural window is a few instructions wide; the flake is rare);
- before, window held open: 0/32 (deterministic failure, "should be 1 but
  was 2");
- after, window held open: 40/40.
Full TwinCAT suite 663/663 at AKKA_TEST_TIMEFACTOR 1 and 3.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:29:33 -05:00

36 lines
1.4 KiB
C#

using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
/// <summary>Collects the driver's Warning-level messages (thread-safe) for once-per-X assertions.</summary>
internal sealed class TwinCATCapturingLogger : ILogger<TwinCATDriver>
{
private readonly List<string> _warnings = [];
/// <summary>Gets a snapshot of the formatted Warning messages logged so far.</summary>
public IReadOnlyList<string> Warnings { get { lock (_warnings) return [.. _warnings]; } }
/// <summary>
/// Optional hook run synchronously on the logging thread for each Warning, after it is captured. A test can
/// block in it to hold open the window between the driver's log call and its next statement.
/// </summary>
public Action<string>? OnWarning { get; init; }
/// <inheritdoc />
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (logLevel != LogLevel.Warning) return;
var message = formatter(state, exception);
lock (_warnings) _warnings.Add(message);
OnWarning?.Invoke(message);
}
}