3 Commits

Author SHA1 Message Date
897233c75f Merge pull request 'fix(bridge): dial the configured port, and back off when refused (D154, D155)' (#15) from fix/config-before-link into main
All checks were successful
Release plugin / release (push) Successful in 16s
Reviewed-on: #15
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-09-26 06:27:02 +00:00
3e928bb2f6 fix(bridge): a backoff that holds against a sidecar that refuses (D155)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m38s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 01:19:23 -05:00
04491e70af fix(bridge): read the config before the link thread dials (D154)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in -1m29s
Init() started LinkLoop before Loaded() read the config, so the thread's
first dial saw _config == null and used the 7799 fallback on both Oxide and
Carbon. One server per host made that its own sidecar by coincidence. With
two on a host (the installer's named instances, phase 18), the second
server dialled the FIRST server's sidecar, sat in its accept backlog, and
the moment the first server's plugin reloaded that sidecar took the second
server's connection: the website showed server "alpha" with beta's
hostname and wipe.

LoadConfigValues() now runs in Init() before the thread starts; Loaded()
keeps its log line and the lease/world/reward loads.

Walked on the phase 18 rig: three instances (two Oxide, one Carbon 7801),
each now dials its own port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-26 01:07:41 -05:00

View File

@@ -65,6 +65,15 @@ namespace Oxide.Plugins
/// </summary> /// </summary>
private const int MaxBackoffMs = 5000; private const int MaxBackoffMs = 5000;
/// <summary>
/// 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.
/// </summary>
private const int StableLinkMs = 10000;
/// <summary>The longest line accepted from the sidecar. Over-long lines are discarded.</summary> /// <summary>The longest line accepted from the sidecar. Over-long lines are discarded.</summary>
private const int MaxInboundLineBytes = 1024 * 1024; private const int MaxInboundLineBytes = 1024 * 1024;
@@ -244,6 +253,13 @@ namespace Oxide.Plugins
_bootId = ResolveBootId(); _bootId = ResolveBootId();
_wipeId = ResolveWipeId(); _wipeId = ResolveWipeId();
// Before the link thread, not in `Loaded`: both frameworks call `Init` first, and a
// thread started here with `_config` still null dials the 7799 fallback. With one
// server per host that is its own sidecar by coincidence; with two it is the OTHER
// server's, whose sidecar then files this server's history under that one's id
// (found by the phase 18 walk, D154).
LoadConfigValues();
_running = true; _running = true;
// Expired link codes are dropped on a timer, not on lookup: an unconfirmed code is // Expired link codes are dropped on a timer, not on lookup: an unconfirmed code is
@@ -496,7 +512,6 @@ namespace Oxide.Plugins
private void Loaded() private void Loaded()
{ {
LoadConfigValues();
Puts($"protocol {ProtocolVersion}, serverId '{_config.ServerId}', sidecar {_config.Host}:{_config.Port}"); Puts($"protocol {ProtocolVersion}, serverId '{_config.ServerId}', sidecar {_config.Host}:{_config.Port}");
// Here rather than at `OnServerInitialized`: the link thread starts in `Init`, so a // Here rather than at `OnServerInitialized`: the link thread starts in `Init`, so a
@@ -564,6 +579,7 @@ namespace Oxide.Plugins
{ {
TcpClient client = null; TcpClient client = null;
Thread reader = null; Thread reader = null;
System.Diagnostics.Stopwatch upFor = null;
int epoch = Interlocked.Increment(ref _epoch); int epoch = Interlocked.Increment(ref _epoch);
@@ -582,7 +598,7 @@ namespace Oxide.Plugins
_dead = false; _dead = false;
_connected = true; _connected = true;
_loggedConnectFailure = false; // so a LATER outage is reported again _loggedConnectFailure = false; // so a LATER outage is reported again
backoffMs = 500; upFor = System.Diagnostics.Stopwatch.StartNew();
Interlocked.Increment(ref _connects); Interlocked.Increment(ref _connects);
Puts($"connected to {host}:{port}"); Puts($"connected to {host}:{port}");
@@ -654,14 +670,25 @@ namespace Oxide.Plugins
if (!_running) if (!_running)
break; 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. // 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 // A handle, not `Thread.Sleep`: Unload runs on the MAIN thread and joins this one,
// an uninterruptible sleep here is a stall of up to MaxBackoffMs on every reload — // 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 // — 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 // that whole time. Unload sets `_stopping`, so waiting on it turns that stall into
// the handle turns that stall into an immediate return. // an immediate return.
Wake.WaitOne(backoffMs); //
// `_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); backoffMs = Math.Min(backoffMs * 2, MaxBackoffMs);
} }