fix(focas): per-device connect-attempt backoff across the three loops (05/STAB-8)
This commit is contained in:
@@ -629,7 +629,9 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
var client = await EnsureConnectedAsync(state, ct).ConfigureAwait(false);
|
||||
// The probe bypasses the connect backoff — its own interval is the bounded recovery
|
||||
// cadence, and a successful probe connect resets the window for the data + loop paths.
|
||||
var client = await EnsureConnectedAsync(state, ct, bypassBackoff: true).ConfigureAwait(false);
|
||||
// Apply Probe.Timeout so a hung CNC socket gets cancelled at the configured
|
||||
// budget rather than blocking until the OS TCP timeout.
|
||||
// TimeSpan.Zero / negative means "no per-probe timeout" — fall back to the loop
|
||||
@@ -1120,10 +1122,24 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
return _options.Devices.FirstOrDefault()?.HostAddress ?? DriverInstanceId;
|
||||
}
|
||||
|
||||
private async Task<IFocasClient> EnsureConnectedAsync(DeviceState device, CancellationToken ct)
|
||||
/// <summary>
|
||||
/// Thrown when a per-device <see cref="ConnectionBackoff"/> window is still open (05/STAB-8)
|
||||
/// — a fail-fast that costs no wire traffic. Read/write map it to <c>BadCommunicationError</c>
|
||||
/// via their generic catch, exactly as a real connect failure.
|
||||
/// </summary>
|
||||
private sealed class ConnectionThrottledException(string message) : Exception(message);
|
||||
|
||||
private async Task<IFocasClient> EnsureConnectedAsync(
|
||||
DeviceState device, CancellationToken ct, bool bypassBackoff = false)
|
||||
{
|
||||
if (device.Client is { IsConnected: true } c) return c;
|
||||
|
||||
// 05/STAB-8: inside an open backoff window a data-path / per-tick-loop caller fails fast — no
|
||||
// socket create, no wire connect. The probe loop bypasses this and drives failure/success.
|
||||
if (!bypassBackoff && !device.Backoff.ShouldAttempt(DateTime.UtcNow))
|
||||
throw new ConnectionThrottledException(
|
||||
$"FOCAS connect to {device.Options.HostAddress} throttled — backoff window open after a recent connect failure.");
|
||||
|
||||
// Discard the existing client (if any) before creating a new one. A client that is
|
||||
// non-null but not connected may have been disposed by a HandleRecycle race or a prior
|
||||
// teardown — retrying ConnectAsync on a disposed FocasWireClient hits ThrowIfDisposed and
|
||||
@@ -1148,8 +1164,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
{
|
||||
device.Client.Dispose();
|
||||
device.Client = null;
|
||||
device.Backoff.RecordFailure(DateTime.UtcNow); // 05/STAB-8: open the backoff window
|
||||
throw;
|
||||
}
|
||||
device.Backoff.RecordSuccess(); // 05/STAB-8: recovery resets the window immediately
|
||||
return device.Client;
|
||||
}
|
||||
|
||||
@@ -1195,6 +1213,17 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
/// <summary>Gets or sets the FOCAS client instance.</summary>
|
||||
public IFocasClient? Client { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-device connect-attempt throttle (05/STAB-8). A dead CNC fails fast inside the
|
||||
/// backoff window instead of paying a full two-socket reconnect on every tick of each of
|
||||
/// the read / write / probe / fixed-tree / recycle loops; the probe loop bypasses it and a
|
||||
/// successful connect resets it. FOCAS's <c>EnsureConnectedAsync</c> is not gate-serialized
|
||||
/// (the loops already race the client swap by design), so this is best-effort — atomic
|
||||
/// enough on 64-bit for the int/DateTime state, and a benign extra/skipped attempt is the
|
||||
/// worst outcome of a race.
|
||||
/// </summary>
|
||||
public ConnectionBackoff Backoff { get; } = new(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30));
|
||||
|
||||
/// <summary>Gets the lock object for probe synchronization.</summary>
|
||||
public object ProbeLock { get; } = new();
|
||||
/// <summary>Gets or sets the current host connectivity state.</summary>
|
||||
|
||||
Reference in New Issue
Block a user