Skip to content

fix(run-engine): record system failure when dequeue recovery nack dead-letters run (#5106) - #5113

Closed
granbellllc-png wants to merge 1 commit into
triggerdotdev:mainfrom
granbellllc-png:fix/issue-5106-dequeue-recovery-dead-letter
Closed

granbellllc-png wants to merge 1 commit into
triggerdotdev:mainfrom
granbellllc-png:fix/issue-5106-dequeue-recovery-dead-letter

Conversation

@granbellllc-png

Copy link
Copy Markdown

Resolves #5106

Summary

In DequeueSystem.dequeueFromWorkerQueue, when preparing a run for dequeue encounters an error, the recovery catch path contains two fallback branches that invoke this.$.runQueue.nackMessage:

  1. The first branch (when the execution snapshot cannot be read) checks the return value of nackMessage:
if (snapshotError || !read) {
  const requeued = await this.$.runQueue.nackMessage({ orgId, messageId: runId });
  if (requeued === false) {
    await this.runAttemptSystem.systemFailure({ runId, organizationId: orgId, error: dequeueMaxRetriesError, tx: prisma });
  }
  return;
}
  1. The second branch (when findRun fails or the environment cannot be resolved) previously dropped the return value of nackMessage:
if (findError || !run || !env) {
  this.$.logger.error("... Failed to find run, nacking directly via Redis", { runId, orgId, findError });
  await this.$.runQueue.nackMessage({ orgId, messageId: runId });
  return;
}

Because nackMessage moves the message to the dead-letter queue (DLQ) and returns false once attempt >= maxAttempts, ignoring the return value left the run indefinitely in QUEUED/PENDING status with no queue message and no failure recorded. Additionally, controlPlaneResolver.resolveEnv was awaited outside of tryCatch, risking unhandled rejections that could escape the recovery lock without triggering a nack.

Solution

  1. In DequeueSystem.dequeueFromWorkerQueue:
    • Wrapped controlPlaneResolver.resolveEnv in tryCatch so environment resolution rejections are handled safely within the recovery branch.
    • Evaluated const requeued = await this.$.runQueue.nackMessage({ orgId, messageId: runId }).
    • If requeued === false (dead-lettered), call this.runAttemptSystem.systemFailure with dequeueMaxRetriesError and tx: prisma, ensuring the run transitions to SYSTEM_FAILURE and the snapshot to FINISHED.
  2. Added unit/container regression test dequeueRecoveryNackDlq.test.ts verifying that when findRun rejects and the message is moved to DLQ, the run is transitioned to SYSTEM_FAILURE.

…d-letters run (triggerdotdev#5106)

In DequeueSystem.dequeueFromWorkerQueue recovery path, when the run read or resolveEnv fails (e.g. during a datastore outage or when runStore.findRun rejects), the code previously called runQueue.nackMessage without inspecting the return value. When attempt >= maxAttempts, nackMessage returns false and dead-letters the message into DLQ, leaving the run indefinitely in QUEUED/PENDING status with no failure recorded.

This fix:
1. Wraps controlPlaneResolver.resolveEnv in tryCatch to prevent unhandled rejections from escaping the recovery lock.
2. Checks the boolean result of runQueue.nackMessage: if requeued === false, calls runAttemptSystem.systemFailure with dequeueMaxRetriesError (matching the behavior of the snapshot read recovery branch).
3. Adds regression test dequeueRecoveryNackDlq.test.ts verifying that dead-lettered runs transition to SYSTEM_FAILURE and snapshot to FINISHED.
@changeset-bot

changeset-bot Bot commented Nov 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 0ae23cd

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

Copy link
Copy Markdown
Contributor

Hi @granbellllc-png, thanks for your interest in contributing!

This project requires that pull request authors are vouched, and you are not in the list of vouched users.

This PR will be closed automatically. See https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md for more details.

@github-actions github-actions Bot closed this Nov 11, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment on lines +30 to +46
const store = new Proxy(base, {
get(target, prop) {
const value = Reflect.get(target, prop, target);
if (prop === "findRun") {
return (...args: any[]) => {
// Dequeue preparation (dequeueRunSelect) and the recovery read both select
// runtimeEnvironmentId; the snapshot helper's fallback read only selects { id }.
// The real findRun is async, so a store outage is a rejected promise, never a sync throw.
if (armed && args[1]?.select?.runtimeEnvironmentId) {
injected++;
return Promise.reject(new Error("injected run-store outage"));
}
return (value as Function).apply(target, args);
};
}
return typeof value === "function" ? value.bind(target) : value;
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Regression test uses a stubbed store

When armed is true, the test's store fabricates rejected findRun calls. The repository forbids stubs and fakes in tests, even alongside real containers.

Learn more

The test passes a proxy as its run store. Its findRun override fabricates a failure while the other methods delegate to the real store. Test rules prohibit mocks, stubs and fakes; AGENTS.md requires using testcontainers instead. The failure injection is the only way this test reaches its target branch, so replacing only the proxy lines will not preserve its coverage.

Example: Once armed becomes true, the test's findRun rejects without querying PostgreSQL; the run reaches recovery because of a simulated store error, not a real store failure.

Recommended fix: Exercise recovery with a genuine database or resolver failure using container-backed dependencies, without replacing findRun behavior with a stub. Preserve the DLQ and final-state assertions.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +23 to +25
describe("dequeue recovery nack (#5106)", () => {
containerTest(
"a run whose message is dead-lettered by the recovery nack is failed, not left QUEUED",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Regression test omits required comment

The new regression test lacks the mandatory one-line // Catches: <bug> comment. Its reproduction header does not use the required format.

Suggested change
describe("dequeue recovery nack (#5106)", () => {
containerTest(
"a run whose message is dead-lettered by the recovery nack is failed, not left QUEUED",
// Catches: dead-lettered dequeue recovery leaves the run queued instead of recording system failure.
describe("dequeue recovery nack (#5106)", () => {
containerTest(
"a run whose message is dead-lettered by the recovery nack is failed, not left QUEUED",

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: a run whose queue message is dead-lettered by the dequeue recovery path is left QUEUED with no failure recorded

1 participant