@boardwalk-labs/runner 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ /** Discard the global HTTP connection pool so post-restore requests open fresh sockets. Never
2
+ * throws (a wake must not fail on pool hygiene) and never blocks (the dead pool drains in the
3
+ * background). Returns true if the dispatcher was swapped. */
4
+ export declare function resetHttpConnectionPool(): boolean;
@@ -0,0 +1,49 @@
1
+ // Reset the global HTTP connection pool after a snapshot restore.
2
+ //
3
+ // The North Star substrate freezes the whole VM (memory + every socket) and restores it later,
4
+ // possibly minutes or days on. Keep-alive TCP sockets do NOT survive that: undici pools them, but
5
+ // on restore the frozen socket state is stale and the peer — the per-host egress proxy (a CONNECT
6
+ // tunnel) and, through it, the broker — long since closed its end. undici would REUSE such a socket
7
+ // for the next request, which then hangs (no bytes flow, no RST arrives on a half-dead tunnel) until
8
+ // the control client's 30s AbortSignal.timeout fires and aborts it. On a woken run's FIRST broker
9
+ // call (finalize) that abort crashes the run — the "socket is dead but never reset" gap the control
10
+ // client's timeout only papered over (see runner_control_client.ts).
11
+ //
12
+ // The fix is to discard the dead pool on resume. Swapping in a fresh EnvHttpProxyAgent — which
13
+ // re-reads the unchanged HTTP(S)_PROXY env, so proxying is preserved — means every post-wake request
14
+ // opens a NEW socket instead of reusing a frozen one. Node's global `fetch` reads the same global
15
+ // dispatcher, so this covers the control client, inference, artifacts, and any author fetch alike.
16
+ import { EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
17
+ import { createLogger } from "./support/index.js";
18
+ const log = createLogger("http_pool_reset");
19
+ /** Discard the global HTTP connection pool so post-restore requests open fresh sockets. Never
20
+ * throws (a wake must not fail on pool hygiene) and never blocks (the dead pool drains in the
21
+ * background). Returns true if the dispatcher was swapped. */
22
+ export function resetHttpConnectionPool() {
23
+ try {
24
+ const prev = getGlobalDispatcher();
25
+ setGlobalDispatcher(new EnvHttpProxyAgent());
26
+ // Drain the old pool's dead sockets in the background; never block the wake on it.
27
+ const drain = async () => {
28
+ try {
29
+ await prev?.close?.();
30
+ }
31
+ catch {
32
+ try {
33
+ await prev?.destroy?.(new Error("http pool reset on wake"));
34
+ }
35
+ catch {
36
+ /* already gone */
37
+ }
38
+ }
39
+ };
40
+ void drain();
41
+ return true;
42
+ }
43
+ catch (err) {
44
+ log.warn("http_pool_reset_failed", {
45
+ error: err instanceof Error ? err.message : String(err),
46
+ });
47
+ return false;
48
+ }
49
+ }
@@ -38,6 +38,7 @@ import { parseByoProviders } from "./direct_inference.js";
38
38
  import { applyIdentityToEnv, connectIdentityRelayFd, relayFdFromEnv, workerDiagnostics, } from "./identity_relay.js";
39
39
  import { FreezeCoordinator } from "./freeze_coordinator.js";
40
40
  import { reseedUserspaceCsprng } from "./uniqueness_reseed.js";
41
+ import { resetHttpConnectionPool } from "./http_pool_reset.js";
41
42
  import { WorkerWorkflowHost } from "./workflow_host.js";
42
43
  import { BrowserSessionManager } from "./browser_session.js";
43
44
  import { loadGuestBrowserConfig, makeGuestBrowserBackend, connectSessionMcp, } from "./browser_session_backend.js";
@@ -393,6 +394,10 @@ export function assembleWorkerDeps(runtime) {
393
394
  // BEFORE the seam resolves, so no woken author code draws from the stale (pre-suspend) DRBG.
394
395
  onAfterWake: (wake) => {
395
396
  reseedUserspaceCsprng();
397
+ // Keep-alive sockets (to the egress proxy + through it to the broker) do NOT survive the
398
+ // freeze; discard the connection pool so the first post-wake broker call (finalize) opens a
399
+ // fresh socket instead of hanging on a stale one until its 30s timeout crashes the run.
400
+ resetHttpConnectionPool();
396
401
  broker.swapRunToken(wake.run_token);
397
402
  redactor.record(wake.run_token);
398
403
  if (wake.api_token !== undefined && wake.api_token !== "") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boardwalk-labs/runner",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Boardwalk self-hosted runner: execute cloud-scheduled runs on your own machines. Currently: the canonical runner contract types.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -58,6 +58,7 @@
58
58
  "@modelcontextprotocol/sdk": "^1.29.0",
59
59
  "node-gyp-build": "^4.8.4",
60
60
  "tar": "^7.5.16",
61
+ "undici": "^6.27.0",
61
62
  "zod": "^4.0.0"
62
63
  },
63
64
  "devDependencies": {