From 3e928bb2f6f61c50180b3644cb6ee64893f21e58 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 26 Sep 2026 01:19:23 -0500 Subject: [PATCH] fix(bridge): a backoff that holds against a sidecar that refuses (D155) The reconnect backoff waited on `Wake`, which `Emit` signals on every frame, so it lasted only until the next hook fired. That was invisible while a failed connect was silent; against a sidecar that now refuses a plugin naming another server (D155) the phase 18 walk measured some 250 redials a second, each printing two console lines and rebuilding the boards. It now waits on `_stopping`, the handle that already exists so a CONNECT is not abandoned by `Emit`; Unload still ends the wait at once. The backoff also resets only after a link has held for 10 s (StableLinkMs). A connection closed on arrival used to reset it to 500 ms on every accept, so a refusal loop never backed off at all. Walked: a plugin aimed at another server's sidecar is refused every 5 s (the ceiling), not continuously; a stable link whose sidecar restarts is back in 0.49 s, as before; no Unload stall on reload. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- overlay/oxide/plugins/RunicGateway.cs | 35 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs index fd998f3..b8595ae 100644 --- a/overlay/oxide/plugins/RunicGateway.cs +++ b/overlay/oxide/plugins/RunicGateway.cs @@ -65,6 +65,15 @@ namespace Oxide.Plugins /// private const int MaxBackoffMs = 5000; + /// + /// How long a link must stay up before its drop counts as an outage rather than a + /// refusal. The backoff resets only past it: a sidecar that closes every connection at + /// once — one refusing a plugin that names another server (D155), or one crash-looping — + /// would otherwise be redialled twice a second for ever, rebuilding the boards on the + /// main thread and printing two console lines each time. + /// + private const int StableLinkMs = 10000; + /// The longest line accepted from the sidecar. Over-long lines are discarded. private const int MaxInboundLineBytes = 1024 * 1024; @@ -570,6 +579,7 @@ namespace Oxide.Plugins { TcpClient client = null; Thread reader = null; + System.Diagnostics.Stopwatch upFor = null; int epoch = Interlocked.Increment(ref _epoch); @@ -588,7 +598,7 @@ namespace Oxide.Plugins _dead = false; _connected = true; _loggedConnectFailure = false; // so a LATER outage is reported again - backoffMs = 500; + upFor = System.Diagnostics.Stopwatch.StartNew(); Interlocked.Increment(ref _connects); Puts($"connected to {host}:{port}"); @@ -660,14 +670,25 @@ namespace Oxide.Plugins if (!_running) break; + // A link that held is an outage: start again from the shortest wait. One that was + // closed on arrival keeps doubling (see StableLinkMs). + if (upFor != null && upFor.ElapsedMilliseconds >= StableLinkMs) + backoffMs = 500; + // Nothing is listening yet, or the sidecar restarted. Both are normal. // - // `Wake`, not `Thread.Sleep`: Unload runs on the MAIN thread and joins this one, so - // an uninterruptible sleep here is a stall of up to MaxBackoffMs on every reload — - // Oxide reports it as `Calling 'Unload' took 1918ms`, and the server is frozen for - // that whole time. Unload sets `_running` false and signals `Wake`, so waiting on - // the handle turns that stall into an immediate return. - Wake.WaitOne(backoffMs); + // A handle, not `Thread.Sleep`: Unload runs on the MAIN thread and joins this one, + // so an uninterruptible sleep here is a stall of up to MaxBackoffMs on every reload + // — Oxide reports it as `Calling 'Unload' took 1918ms`, and the server is frozen for + // that whole time. Unload sets `_stopping`, so waiting on it turns that stall into + // an immediate return. + // + // `_stopping`, not `Wake`, for the reason `_stopping` gives: `Emit` signals `Wake` + // on every frame, so a backoff waited on it lasted until the next hook fired. That + // was invisible while a failed connect was silent; against a sidecar that refuses + // (D155) the phase 18 walk measured some 250 redials a second, each printing two + // console lines and rebuilding the boards. + _stopping.WaitOne(backoffMs); backoffMs = Math.Min(backoffMs * 2, MaxBackoffMs); }