Skip to content

Commit 1a033b6

Browse files
authored
fix(webapp,core): retry run resume through transient database outages (#4161)
## Summary When the platform database is briefly unreachable while a run is resuming from a wait, the run no longer fails with `TASK_EXECUTION_ABORTED`. The worker now retries the resume through the outage instead of aborting on the first blip. ## Root cause Resuming a run calls the engine's `continue` worker-action endpoint. That route caught every error and returned a `422`, which the worker's HTTP client treats as non-retryable. So a transient Prisma infrastructure error (for example `P1001` "Can't reach database server") was flattened into a permanent failure: the worker gave up, force-killed the run process, and completed it with `TASK_EXECUTION_ABORTED`. ## Fix - The `continue` route now lets infrastructure errors propagate to the generic 500 handler (message scrubbed, and retryable by the worker's HTTP client), the same treatment the trigger path already gives them via `isInfrastructureError`. Genuine validation errors (snapshot mismatch, invalid state) still return `422`, so a stale retry stays non-retryable. Resuming is idempotent server-side (guarded by the snapshot id), so retrying is safe. - The worker's `continueRunExecution` calls (both the runner-to-supervisor and supervisor-to-engine hops) retry with a longer, jittered backoff so they can ride out an outage lasting tens of seconds, and the jitter keeps a fleet of resuming runs from stampeding the database the moment it recovers. Builds on #3960, which scrubbed the leaked message on these routes but left the status non-retryable. No changeset: this is a server-side behaviour fix recorded via `.server-changes`. The `@trigger.dev/core` edits are internal run-engine worker plumbing, not a public API change.
1 parent 018f445 commit 1a033b6

4 files changed

Lines changed: 51 additions & 2 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Runs resuming after a wait no longer fail with TASK_EXECUTION_ABORTED when the database is briefly unreachable; the resume endpoint returns a retryable response for transient infrastructure errors instead of a permanent one.

apps/webapp/app/routes/engine.v1.worker-actions.runs.$runFriendlyId.snapshots.$snapshotFriendlyId.continue.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { WorkerApiContinueRunExecutionRequestBody } from "@trigger.dev/core
44
import { z } from "zod";
55
import { logger } from "~/services/logger.server";
66
import { createLoaderWorkerApiRoute } from "~/services/routeBuilders/apiBuilder.server";
7-
import { clientSafeErrorMessage } from "~/utils/prismaErrors";
7+
import { clientSafeErrorMessage, isInfrastructureError } from "~/utils/prismaErrors";
88

99
export const loader = createLoaderWorkerApiRoute(
1010
{
@@ -31,7 +31,21 @@ export const loader = createLoaderWorkerApiRoute(
3131

3232
return json(continuationResult);
3333
} catch (error) {
34-
logger.warn("Failed to suspend run", { runFriendlyId, snapshotFriendlyId, error });
34+
logger.warn("Failed to continue run execution", {
35+
runFriendlyId,
36+
snapshotFriendlyId,
37+
error,
38+
});
39+
40+
// A Prisma infrastructure error (e.g. P1001 "Can't reach database
41+
// server") means the DB was transiently unreachable while resuming. A 422
42+
// is non-retryable, so the worker would permanently abort a run over a
43+
// blip. Let it propagate to the generic 500 handler, which scrubs the
44+
// message and is retried by the worker's HTTP client.
45+
if (isInfrastructureError(error)) {
46+
throw error;
47+
}
48+
3549
if (error instanceof Error) {
3650
throw json({ error: clientSafeErrorMessage(error) }, { status: 422 });
3751
}

packages/core/src/v3/runEngineWorker/supervisor/http.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,21 @@ export class SupervisorHttpClient {
245245
...this.defaultHeaders,
246246
...this.runnerIdHeader(runnerId),
247247
},
248+
},
249+
{
250+
// This is the hop that reaches the engine, so it's where a transient
251+
// database outage during resume surfaces (as a retryable 5xx). Resuming
252+
// is idempotent server-side (guarded by the snapshot id), so retry
253+
// generously to ride out the outage rather than aborting the run.
254+
// `randomize` jitters the delay so a fleet of runs resuming at once
255+
// doesn't stampede the DB the moment it recovers.
256+
retry: {
257+
minTimeoutInMs: 500,
258+
maxTimeoutInMs: 10_000,
259+
maxAttempts: 8,
260+
factor: 2,
261+
randomize: true,
262+
},
248263
}
249264
);
250265
}

packages/core/src/v3/runEngineWorker/workload/http.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ export class WorkloadHttpClient {
132132
headers: {
133133
...this.defaultHeaders(),
134134
},
135+
},
136+
{
137+
// This hop only reaches the supervisor's workload server, so retry
138+
// generously with jittered backoff to ride out a transient blip
139+
// talking to the supervisor (e.g. a restart) rather than aborting the
140+
// run. Database outages surface one hop further in, on the
141+
// supervisor-to-engine call, which carries its own retry for them.
142+
retry: {
143+
minTimeoutInMs: 500,
144+
maxTimeoutInMs: 10_000,
145+
maxAttempts: 8,
146+
factor: 2,
147+
randomize: true,
148+
},
135149
}
136150
)
137151
);

0 commit comments

Comments
 (0)