Skip to content

Commit 910a926

Browse files
ff137cursoragent
andcommitted
feat(queue): exit runInLoop after MAX_CONSECUTIVE_FAILURES
After sustained failure the worker should still exit so PM2 can do a clean-slate restart (fresh Knex pool, cleared in-memory state), rather than retrying at the 30s backoff cap forever. Threshold: 10 consecutive failures. With the existing backoff curve (0.5s, 1s, 2s, 4s, 8s, 16s, 30s, 30s, ...) that is 2 -- 2.5 minutes of sustained failure before exiting -- well past any transient DB blip but short enough that a genuinely-broken worker still surfaces via crash + restart instead of silently retrying forever. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9b50630 commit 910a926

1 file changed

Lines changed: 20 additions & 3 deletions

File tree

svc/store/queue.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,26 @@ export async function runReliableQueue(
121121

122122
/**
123123
* Runs an async function on a loop, waiting the delay between each iteration.
124+
*
124125
* On failure, logs and sleeps with exponential backoff + jitter (capped at 30s)
125126
* before retrying, so a sick dependency (e.g. Postgres at max_connections) is
126-
* not hammered by tight-loop restarts. The lastRun health beacon is only
127-
* refreshed on success, so the existing HEALTH_TIMEOUT alerting still detects
128-
* a stuck worker.
127+
* not hammered by tight-loop restarts.
128+
*
129+
* After MAX_CONSECUTIVE_FAILURES in a row, exits the process so PM2 can do a
130+
* clean-slate restart (fresh Knex pool, cleared in-memory state). With the
131+
* default backoff curve this is ~7.5 minutes of sustained failure before
132+
* giving up, which is well past any transient DB blip.
133+
*
134+
* The lastRun health beacon is only refreshed on success, so the existing
135+
* HEALTH_TIMEOUT alerting still detects a stuck worker before exit.
136+
*
129137
* @param func
130138
* @param delay
131139
*/
132140
export async function runInLoop(func: () => Promise<void>, delay: number) {
133141
const BASE_BACKOFF_MS = 250;
134142
const MAX_BACKOFF_MS = 30_000;
143+
const MAX_CONSECUTIVE_FAILURES = 10;
135144
let consecutiveFailures = 0;
136145
while (true) {
137146
console.log("running %s", func.name);
@@ -146,6 +155,14 @@ export async function runInLoop(func: () => Promise<void>, delay: number) {
146155
consecutiveFailures,
147156
e,
148157
);
158+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
159+
console.error(
160+
"%s exceeded %d consecutive failures, exiting for PM2 restart",
161+
func.name,
162+
MAX_CONSECUTIVE_FAILURES,
163+
);
164+
process.exit(1);
165+
}
149166
// 2 ** capped so the exponent doesn't overflow on long outages
150167
const exp = Math.min(consecutiveFailures, 10);
151168
const backoff = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** exp);

0 commit comments

Comments
 (0)