mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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:
co-authored by
Claude Fable 5
parent
c2fbc66bdd
commit
39a617d5a4
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user