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:
Joseph Doherty
2026-05-14 13:04:30 -04:00
parent b3b8313e9c
commit 1a2856526a
60 changed files with 750 additions and 811 deletions
@@ -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=&lt;previous&gt;, RecoveryAttempts&gt;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">