f0992394b6
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
36 lines
1.4 KiB
C#
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);
|
|
}
|
|
}
|