Cap seed API stall with a shared raced budget and explicit CLI exit

Abort signals do not cancel the TCP connect phase, so an unreachable API
stalled the seed ~10s before degrading. All API calls now share one
deadline, the roll fetch races it, and the CLI exits explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-22 00:39:16 -07:00
co-authored by Claude Fable 5
parent c2fbc66bdd
commit 39a617d5a4
+19 -3
View File
@@ -79,6 +79,13 @@ const here = dirname(fileURLToPath(import.meta.url));
const CATALOG_DIR = process.env.IMPECCABLE_CATALOG_DIR || here;
const API_BASE = (process.env.IMPECCABLE_API_URL || 'https://impeccable.style/api').replace(/\/$/, '');
const API_TIMEOUT_MS = Number(process.env.IMPECCABLE_API_TIMEOUT || 4000);
// All API calls in one seed run share a single deadline so an unreachable
// network degrades after one timeout total, never one timeout per call.
let apiDeadline = null;
function apiBudgetMs() {
if (apiDeadline === null) apiDeadline = Date.now() + API_TIMEOUT_MS;
return Math.max(0, apiDeadline - Date.now());
}
const localStates = new Map();
function loadLocal(catalogDir = CATALOG_DIR) {
@@ -120,9 +127,15 @@ async function fetchRoll({ scope, key, mode, reroll }) {
const params = new URLSearchParams({ scope, key, reroll: String(reroll) });
if (mode) params.set('mode', mode);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
const timer = setTimeout(() => controller.abort(), apiBudgetMs());
try {
const response = await fetch(`${API_BASE}/roll?${params}`, { signal: controller.signal });
// Race the budget explicitly: abort signals do not reliably cancel the
// TCP connect phase, so a blackholed route would otherwise stall ~10s.
const response = await Promise.race([
fetch(`${API_BASE}/roll?${params}`, { signal: controller.signal }),
new Promise(resolveTimeout => setTimeout(() => resolveTimeout(null), apiBudgetMs())),
]);
if (!response) return null;
if (!response.ok) return null;
const roll = await response.json();
if (!Array.isArray(roll.challengers) || roll.challengers.length === 0) return null;
@@ -143,7 +156,7 @@ function telemetryDisabled() {
export async function pingChosen({ chosenId, key, scope, mode }) {
if (telemetryDisabled() || !chosenId) return false;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
const timer = setTimeout(() => controller.abort(), apiBudgetMs());
try {
await fetch(`${API_BASE}/chosen`, {
method: 'POST',
@@ -543,4 +556,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
// A raced-out fetch may still hold a socket; exit explicitly so the CLI
// never lingers on a dead network path after output is written.
process.exit(process.exitCode ?? 0);
}