auto-refill: add error categorization, exponential backoff, and fatal error handling

This commit is contained in:
redshift
2026-05-23 23:13:52 +08:00
parent 92ca9a7cb0
commit e5f5c0995d
+56 -1
View File
@@ -15,6 +15,21 @@ export interface AutoRefillConfig {
amount: number;
/** Minimum time between refills (milliseconds) */
cooldownMs: number;
/** Maximum consecutive failures before stopping (0 = never stop on failures) */
maxConsecutiveFailures?: number;
}
// Errors that cannot be resolved by retrying — the user must intervene.
const FATAL_ERROR_PATTERNS = [
/insufficient (?:balance|funds)/i,
/invalid (?:credentials|auth)/i,
/forbidden/i,
/unauthorized/i,
/invoice.*expired/i,
];
function isFatalError(message: string): boolean {
return FATAL_ERROR_PATTERNS.some((pattern) => pattern.test(message));
}
export function startAutoRefillLoop(
@@ -27,6 +42,7 @@ export function startAutoRefillLoop(
let running = true;
let timeout: ReturnType<typeof setInterval> | null = null;
let checkInProgress = false;
let consecutiveFailures = 0;
async function checkAndRefill(): Promise<void> {
if (!running) return;
@@ -50,6 +66,16 @@ export function startAutoRefillLoop(
checkInProgress = true;
// If we've been failing too much, back off rather than retrying at full speed.
// This prevents tight loops on transient errors.
const backoffInterval = Math.min(
intervalMs * Math.pow(2, consecutiveFailures),
5 * 60 * 1000, // cap at 5 minutes
);
if (now - lastRefillAt < backoffInterval) {
return;
}
try {
const balances = await cocod.getBalances();
const totalBalance = Object.values(balances).reduce<number>(
@@ -94,9 +120,38 @@ export function startAutoRefillLoop(
logger.log(`[auto-refill] Fees paid: ${fees_paid} msats`);
}
lastRefillAt = now;
consecutiveFailures = 0; // reset on success
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`[auto-refill] Error: ${message}`);
consecutiveFailures++;
if (isFatalError(message)) {
// Cooldown for 10 minutes — the user likely needs to fund their NWC wallet.
// We don't stop permanently so it auto-recovers without a restart.
const FATAL_COOLDOWN_MS = 10 * 60 * 1000;
logger.error(
`[auto-refill] FATAL: ${message}. The funding wallet (NWC) cannot pay. ` +
`Cooling down for ${FATAL_COOLDOWN_MS / 60000} minutes. ` +
`Fund your NWC wallet and auto-refill will retry automatically.`,
);
lastRefillAt = now; // skip the cooldownMs check, backoff handles the wait
checkInProgress = false;
return;
}
const maxFailures = config.maxConsecutiveFailures ?? 0;
if (maxFailures > 0 && consecutiveFailures >= maxFailures) {
logger.error(
`[auto-refill] Stopping after ${consecutiveFailures} consecutive failures (maxConsecutiveFailures=${maxFailures}). ` +
`Last error: ${message}`,
);
running = false;
return;
}
logger.error(
`[auto-refill] Error (attempt ${consecutiveFailures}, next in ${Math.round(backoffInterval / 1000)}s): ${message}`,
);
} finally {
checkInProgress = false;
}