Skip to content

Commit 4adf547

Browse files
authored
Merge pull request #2959 from ff137/feat/run-in-loop-backoff
Exponential backoff in runInLoop on failure
2 parents 05d01e1 + 48a2976 commit 4adf547

1 file changed

Lines changed: 42 additions & 2 deletions

File tree

svc/store/queue.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,55 @@ export async function runReliableQueue(
123123
}
124124

125125
/**
126-
* Runs an async function on a loop, waiting the delay between each iteration
126+
* Runs an async function on a loop, waiting the delay between each iteration.
127+
*
128+
* On failure, logs and sleeps with exponential backoff + jitter (capped at 30s)
129+
* before retrying, so a sick dependency (e.g. Postgres at max_connections) is
130+
* not hammered by tight-loop restarts.
131+
*
132+
* After MAX_CONSECUTIVE_FAILURES in a row, exits the process so PM2 can do a
133+
* clean-slate restart (fresh Knex pool, cleared in-memory state).
134+
*
135+
* The lastRun health beacon is only refreshed on success, so the existing
136+
* HEALTH_TIMEOUT alerting still detects a stuck worker before exit.
137+
*
127138
* @param func
128139
* @param delay
129140
*/
130141
export async function runInLoop(func: () => Promise<void>, delay: number) {
142+
const BASE_BACKOFF_MS = 250;
143+
const MAX_BACKOFF_MS = 30_000;
144+
const MAX_CONSECUTIVE_FAILURES = 10;
145+
let consecutiveFailures = 0;
131146
while (true) {
132147
console.log("running %s", func.name);
133148
const start = Date.now();
134-
await func();
149+
try {
150+
await func();
151+
} catch (e) {
152+
consecutiveFailures += 1;
153+
console.error(
154+
"%s failed (consecutive failures: %d):",
155+
func.name,
156+
consecutiveFailures,
157+
e,
158+
);
159+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
160+
console.error(
161+
"%s exceeded %d consecutive failures, exiting for PM2 restart",
162+
func.name,
163+
MAX_CONSECUTIVE_FAILURES,
164+
);
165+
process.exit(1);
166+
}
167+
// 2 ** capped so the exponent doesn't overflow on long outages
168+
const exp = Math.min(consecutiveFailures, 10);
169+
const backoff = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** exp);
170+
const jitter = Math.random() * 250;
171+
await new Promise((resolve) => setTimeout(resolve, backoff + jitter));
172+
continue;
173+
}
174+
consecutiveFailures = 0;
135175
const end = Date.now();
136176
console.log("%s: %dms", func.name, end - start);
137177
await redis.setex(

0 commit comments

Comments
 (0)