mbproxy: strip historical phase/wave/plan references from source comments
Comments described the *history* of how the code arrived (phase numbers, wave IDs, review IDs, dated TODOs) instead of what it does today. That scaffolding rotted as the codebase evolved. Cleaned 60 source files + .gitignore; behaviour unchanged (387/387 tests still pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,15 +46,15 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
private volatile string? _lastBindError;
|
||||
private int _recoveryAttempts; // Interlocked
|
||||
|
||||
// Phase 07: current active listener for status-page pair enumeration.
|
||||
// Current active listener for status-page pair enumeration.
|
||||
private volatile PlcListener? _currentListener;
|
||||
|
||||
// Phase 06: _perPlcContext is now mutable so ReplaceContextAsync can swap it.
|
||||
// Access from the accept loop (RunAsync) and from ReplaceContextAsync must be
|
||||
// coherent; we use a volatile reference so the accept loop always reads the latest
|
||||
// context without locking. The PlcListener created on each Polly attempt holds
|
||||
// its own copy of the context at construction time; existing in-flight connections
|
||||
// keep their old reference until they complete.
|
||||
// _perPlcContext is mutable so ReplaceContextAsync can swap it. Access from the accept
|
||||
// loop (RunAsync) and from ReplaceContextAsync must be coherent; we use a volatile
|
||||
// reference so the accept loop always reads the latest context without locking. The
|
||||
// PlcListener created on each Polly attempt holds its own copy of the context at
|
||||
// construction time; existing in-flight connections keep their old reference until they
|
||||
// complete.
|
||||
private volatile PerPlcContext? _currentContext;
|
||||
|
||||
/// <summary>
|
||||
@@ -67,16 +67,15 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
// Phase 12 (W2.15) — completes when the supervisor has transitioned out of Stopped
|
||||
// for the first time (reached Bound or Recovering). Replaces the previous busy-poll
|
||||
// implementation in WaitForInitialBindAttemptAsync, which raced fast Stopped→Bound→
|
||||
// Stopped transitions and never exited if the supervisor task threw inside Polly.
|
||||
// Completes when the supervisor has transitioned out of Stopped for the first time
|
||||
// (reached Bound or Recovering). Used by WaitForInitialBindAttemptAsync to avoid
|
||||
// racing fast Stopped→Bound→Stopped transitions or hanging if the supervisor task
|
||||
// throws inside Polly.
|
||||
//
|
||||
// Phase 12 (W4 / NM4) — non-readonly so StartAsync can re-arm it for a re-Started
|
||||
// supervisor. Without re-arming, a restart-after-stop scenario would have
|
||||
// WaitForInitialBindAttemptAsync return immediately on the previous run's signal,
|
||||
// never observing the new run's bind status. No production caller currently re-Starts,
|
||||
// but the supervisor's state machine should be consistent.
|
||||
// Non-readonly so StartAsync can re-arm it for a re-Started supervisor. Without
|
||||
// re-arming, a restart-after-stop scenario would have WaitForInitialBindAttemptAsync
|
||||
// return immediately on the previous run's signal, never observing the new run's
|
||||
// bind status.
|
||||
private TaskCompletionSource _firstAttemptCompleted = new(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
@@ -104,7 +103,7 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
_multiplexerLogger = multiplexerLogger;
|
||||
_pipeLogger = pipeLogger;
|
||||
_perPlcContext = perPlcContext;
|
||||
_currentContext = perPlcContext; // Phase 06: live context slot
|
||||
_currentContext = perPlcContext; // live context slot
|
||||
_recoveryPipeline = recoveryPipeline;
|
||||
_logger = logger;
|
||||
_backendConnectPipeline = backendConnectPipeline;
|
||||
@@ -121,7 +120,7 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
/// <summary>
|
||||
/// Live collection of active <see cref="UpstreamPipe"/> instances attached to this
|
||||
/// PLC's multiplexer. Returns an empty collection when the listener is not bound.
|
||||
/// Consumed by Phase 07's status page (renamed from <c>ActivePairs</c> in Phase 9).
|
||||
/// Consumed by the status page.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<UpstreamPipe> ActiveUpstreams
|
||||
=> _currentListener?.ActiveUpstreams ?? Array.Empty<UpstreamPipe>();
|
||||
@@ -137,26 +136,25 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
/// </summary>
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
// Phase 12 (W2.16) — refuse to re-Start an already-running or already-disposed
|
||||
// supervisor. After Stop the state machine returns to Stopped and StartAsync
|
||||
// can re-arm; W4/NM3+NM4 below ensure the per-Start state (CTS, TCS) is fresh
|
||||
// each time so no leak or stale signal carries across cycles.
|
||||
// Refuse to re-Start an already-running or already-disposed supervisor. After
|
||||
// Stop the state machine returns to Stopped and StartAsync can re-arm; the per-
|
||||
// Start state (CTS, TCS) is refreshed below so no leak or stale signal carries
|
||||
// across cycles.
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(PlcListenerSupervisor));
|
||||
if (_state != SupervisorState.Stopped || !_supervisorTask.IsCompleted)
|
||||
throw new InvalidOperationException(
|
||||
$"Supervisor for Plc='{_plc.Name}' has already been started.");
|
||||
|
||||
// Phase 12 (W4 / NM3) — dispose the previous CTS before reassigning. The original
|
||||
// code overwrote _supervisorCts unconditionally, leaking the prior CTS on every
|
||||
// re-Start cycle (and any registrations linked to it). Idempotent: ObjectDisposed
|
||||
// catch covers the very-first-Start case where the field-init CTS is still fresh.
|
||||
// Dispose the previous CTS before reassigning so a re-Start cycle does not leak
|
||||
// the prior CTS (and any registrations linked to it). Idempotent: the
|
||||
// ObjectDisposed catch covers the very-first-Start case where the field-init CTS
|
||||
// is still fresh.
|
||||
try { _supervisorCts.Dispose(); } catch (ObjectDisposedException) { /* fresh */ }
|
||||
_supervisorCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
|
||||
// Phase 12 (W4 / NM4) — re-arm the first-attempt TCS so a re-Started supervisor
|
||||
// doesn't immediately observe the previous run's signal in
|
||||
// WaitForInitialBindAttemptAsync.
|
||||
// Re-arm the first-attempt TCS so a re-Started supervisor doesn't immediately
|
||||
// observe the previous run's signal in WaitForInitialBindAttemptAsync.
|
||||
_firstAttemptCompleted = new TaskCompletionSource(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
@@ -170,10 +168,10 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
/// <see cref="SupervisorState.Recovering"/>).
|
||||
/// Returns immediately if the supervisor is already past that point.
|
||||
///
|
||||
/// <para><b>Phase 12 (W2.15)</b> — backed by a <see cref="TaskCompletionSource"/> set
|
||||
/// when the supervisor task first transitions out of <see cref="SupervisorState.Stopped"/>.
|
||||
/// Replaces the previous 10 ms busy-poll which raced fast bind+stop sequences and could
|
||||
/// hang if the supervisor task threw before any state write happened.</para>
|
||||
/// <para>Backed by a <see cref="TaskCompletionSource"/> set when the supervisor task
|
||||
/// first transitions out of <see cref="SupervisorState.Stopped"/>. This avoids both
|
||||
/// racing fast bind+stop sequences and hanging if the supervisor task throws before
|
||||
/// any state write happens.</para>
|
||||
/// </summary>
|
||||
public async Task WaitForInitialBindAttemptAsync(CancellationToken ct)
|
||||
{
|
||||
@@ -184,7 +182,7 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Caller cancelled; not a fault — same observable behaviour as the prior poll.
|
||||
// Caller cancelled; not a fault.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,8 +219,8 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
/// <summary>
|
||||
/// Returns a point-in-time snapshot of this supervisor's state.
|
||||
///
|
||||
/// <para><b>Phase 12 (W2.17)</b> — reads the three observable fields under a single
|
||||
/// lock so the status page can never report inconsistent triples like
|
||||
/// <para>Reads the three observable fields under a single lock so the status page
|
||||
/// can never report inconsistent triples like
|
||||
/// <c>(State=Bound, LastBindError=<previous>, RecoveryAttempts>0)</c>. The
|
||||
/// supervisor task uses <see cref="TransitionTo"/> which takes the same lock, so a
|
||||
/// snapshot reads a transition-consistent view.</para>
|
||||
@@ -241,9 +239,9 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
private readonly object _snapshotLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Phase 12 (W2.17) — atomic three-field transition. State, lastBindError, and
|
||||
/// (optionally) the recoveryAttempts increment all happen under one lock so a
|
||||
/// concurrent <see cref="Snapshot"/> never sees a half-applied transition.
|
||||
/// Atomic three-field transition. State, lastBindError, and (optionally) the
|
||||
/// recoveryAttempts increment all happen under one lock so a concurrent
|
||||
/// <see cref="Snapshot"/> never sees a half-applied transition.
|
||||
/// </summary>
|
||||
private void TransitionTo(SupervisorState newState, string? lastBindError, bool incrementRecoveryAttempt)
|
||||
{
|
||||
@@ -258,15 +256,10 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
|
||||
/// <summary>
|
||||
/// Atomically swaps the per-PLC context (tag map + optional response cache) on the
|
||||
/// running listener AND its live multiplexer.
|
||||
///
|
||||
/// <para><b>Phase 12 (W1.1)</b> — previously this method only updated the supervisor's
|
||||
/// <c>_currentContext</c> slot, which meant the running <see cref="PlcMultiplexer"/>
|
||||
/// kept using the OLD context (it captured the reference at construction). A reload
|
||||
/// only became visible on the next listener fault. Now the swap propagates into the
|
||||
/// running mux via <see cref="PlcMultiplexer.ReplaceContext"/>, so the very next PDU
|
||||
/// sees the new tag map / new cache. Counters are preserved (the new context carries
|
||||
/// the same <c>ProxyCounters</c> instance) so operator history is not reset.</para>
|
||||
/// running listener AND its live multiplexer. The swap propagates into the running
|
||||
/// mux via <see cref="PlcMultiplexer.ReplaceContext"/>, so the very next PDU sees
|
||||
/// the new tag map / new cache. Counters are preserved (the new context carries the
|
||||
/// same <c>ProxyCounters</c> instance) so operator history is not reset.
|
||||
///
|
||||
/// <para><b>Old cache lifecycle</b>: the supervisor disposes the outgoing context's
|
||||
/// cache AFTER the multiplexer has been swapped to the new context. By that point no
|
||||
@@ -281,16 +274,16 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
// subsequent fault recovery) will pick up newCtx through this slot.
|
||||
_currentContext = newCtx;
|
||||
|
||||
// Phase 12 (W1.1) — push the swap into the running multiplexer so existing
|
||||
// connections see the new tag map / new cache on their next PDU. _currentListener
|
||||
// may be null between Polly retry attempts; in that case the next listener built
|
||||
// inside the Polly loop will pick up newCtx through _currentContext above.
|
||||
// Push the swap into the running multiplexer so existing connections see the new
|
||||
// tag map / new cache on their next PDU. _currentListener may be null between
|
||||
// Polly retry attempts; in that case the next listener built inside the Polly loop
|
||||
// will pick up newCtx through _currentContext above.
|
||||
_currentListener?.Multiplexer?.ReplaceContext(newCtx);
|
||||
|
||||
// Phase 12 (W1.1 + W2.8) — drop the outgoing cache AFTER the swap so the running
|
||||
// multiplexer can no longer reach it. Clear() snapshots the entry count for the
|
||||
// mbproxy.cache.flushed log event before disposing the cache (which stops the
|
||||
// eviction loop and releases the timer).
|
||||
// Drop the outgoing cache AFTER the swap so the running multiplexer can no longer
|
||||
// reach it. Clear() snapshots the entry count for the mbproxy.cache.flushed log
|
||||
// event before disposing the cache (which stops the eviction loop and releases
|
||||
// the timer).
|
||||
if (oldCache is not null && !ReferenceEquals(oldCache, newCtx.Cache))
|
||||
{
|
||||
int dropped = oldCache.Clear();
|
||||
@@ -318,11 +311,11 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
// A faulted listener's TcpListener socket must be disposed before
|
||||
// re-binding. We create a new PlcListener on each attempt.
|
||||
//
|
||||
// Phase 06: use _currentContext (volatile) so that a ReplaceContextAsync
|
||||
// call between Polly retry attempts is picked up here. Each listener
|
||||
// captures the context at construction time; existing in-flight pairs
|
||||
// keep their own reference. See ReplaceContextAsync for the transition
|
||||
// window documentation.
|
||||
// Use _currentContext (volatile) so that a ReplaceContextAsync call
|
||||
// between Polly retry attempts is picked up here. Each listener captures
|
||||
// the context at construction time; existing in-flight pairs keep their
|
||||
// own reference. See ReplaceContextAsync for the transition window
|
||||
// documentation.
|
||||
var listener = new PlcListener(
|
||||
_plc,
|
||||
_connectionOptions,
|
||||
@@ -334,7 +327,7 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
_backendConnectPipeline,
|
||||
_coalescingOptions);
|
||||
|
||||
// Phase 07: expose the current listener for status-page pair enumeration.
|
||||
// Expose the current listener for status-page pair enumeration.
|
||||
_currentListener = listener;
|
||||
|
||||
try
|
||||
@@ -351,10 +344,10 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
|
||||
string truncated = Truncate(bindEx.Message, 256);
|
||||
TransitionTo(SupervisorState.Recovering, truncated, incrementRecoveryAttempt: true);
|
||||
// Phase 12 (W2.15) — signal the first transition out of Stopped.
|
||||
// Signal the first transition out of Stopped.
|
||||
_firstAttemptCompleted.TrySetResult();
|
||||
|
||||
// Also update the per-PLC counters if available (Phase 07 reads these).
|
||||
// Also update the per-PLC counters if available (status page reads these).
|
||||
_currentContext?.Counters.IncrementRecoveryAttempt(truncated);
|
||||
|
||||
LogBindFailed(_logger, _plc.Name, _plc.ListenPort, truncated);
|
||||
@@ -379,7 +372,7 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
// Clear the last bind error on a successful bind.
|
||||
TransitionTo(SupervisorState.Bound, lastBindError: null, incrementRecoveryAttempt: false);
|
||||
_currentContext?.Counters.ClearLastBindError();
|
||||
// Phase 12 (W2.15) — signal the first transition out of Stopped.
|
||||
// Signal the first transition out of Stopped.
|
||||
_firstAttemptCompleted.TrySetResult();
|
||||
|
||||
// ── Run the accept loop ──────────────────────────────────────────
|
||||
@@ -407,9 +400,8 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
|
||||
string truncated = Truncate(runEx.Message, 256);
|
||||
TransitionTo(SupervisorState.Recovering, truncated, incrementRecoveryAttempt: true);
|
||||
// Phase 12 (W2.15) — also signal first-attempt-completed in case the
|
||||
// very first listener.RunAsync faulted before the bind-success path
|
||||
// signalled it.
|
||||
// Also signal first-attempt-completed in case the very first
|
||||
// listener.RunAsync faulted before the bind-success path signalled it.
|
||||
_firstAttemptCompleted.TrySetResult();
|
||||
|
||||
// Also update the per-PLC counters if available.
|
||||
@@ -457,16 +449,16 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
_state = SupervisorState.Stopped;
|
||||
}
|
||||
_currentListener = null;
|
||||
// Phase 12 (W2.15) — defensive: if RunSupervisorAsync exits before any bind
|
||||
// attempt fired (e.g. construction-time fault), unblock any awaiting
|
||||
// Defensive: if RunSupervisorAsync exits before any bind attempt fired
|
||||
// (e.g. construction-time fault), unblock any awaiting
|
||||
// WaitForInitialBindAttemptAsync caller so it doesn't hang.
|
||||
_firstAttemptCompleted.TrySetResult();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 12 (W2 cleanup) — single helper for the truncate-exception-message pattern
|
||||
/// previously copy-pasted across three call sites.
|
||||
/// Single helper for the truncate-exception-message pattern shared across the
|
||||
/// supervisor's bind/run/end recovery paths.
|
||||
/// </summary>
|
||||
private static string Truncate(string s, int max) => s.Length > max ? s[..max] : s;
|
||||
|
||||
@@ -487,8 +479,8 @@ internal sealed partial class PlcListenerSupervisor : IAsyncDisposable
|
||||
// Best-effort cleanup.
|
||||
}
|
||||
|
||||
// Phase 11: dispose the response cache (if any) — its eviction timer would
|
||||
// otherwise outlive the supervisor.
|
||||
// Dispose the response cache (if any) — its eviction timer would otherwise
|
||||
// outlive the supervisor.
|
||||
_currentContext?.Cache?.Dispose();
|
||||
|
||||
_supervisorCts.Dispose();
|
||||
|
||||
@@ -26,14 +26,14 @@ public enum SupervisorState
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable point-in-time snapshot of a supervisor's state. Consumed by Phase 07's
|
||||
/// status page via <see cref="PlcListenerSupervisor.Snapshot"/>.
|
||||
/// Immutable point-in-time snapshot of a supervisor's state. Consumed by the status
|
||||
/// page via <see cref="PlcListenerSupervisor.Snapshot"/>.
|
||||
///
|
||||
/// <para><b>RecoveryAttempts semantics</b>: this counter <em>accumulates over the lifetime
|
||||
/// of the supervisor</em> and is never reset. Operators reading the status page should
|
||||
/// interpret it as "how many times has this listener faulted or failed to bind since
|
||||
/// the service started" — useful for detecting port-flapping or repeated OS network
|
||||
/// resets. Phase 07 surfaces it as-is.</para>
|
||||
/// resets.</para>
|
||||
/// </summary>
|
||||
/// <param name="State">Current state of the supervisor.</param>
|
||||
/// <param name="LastBindError">
|
||||
|
||||
Reference in New Issue
Block a user