Skip to content

fix(run-engine,webapp): guard run finalization against lost resume signals - #4849

Merged
matt-aitken merged 6 commits into
mainfrom
fix/run-finalization-guard-tri-13537
Aug 31, 2026
Merged

fix(run-engine,webapp): guard run finalization against lost resume signals#4849
matt-aitken merged 6 commits into
mainfrom
fix/run-finalization-guard-tri-13537

Conversation

@matt-aitken

@matt-aitken matt-aitken commented Aug 31, 2026

Copy link
Copy Markdown
Member

Summary

A run's finish commit and its follow-up side effects (completing the associated waitpoint, waking blocked parents, releasing the queue slot, nudging batch completion) are separate writes across Postgres and Redis. If a database error landed between them, the child run was already finished, so the runner's retries hit the "Run is already finished" guard and the completion signal was lost for good. A parent blocked on triggerAndWait or batchTriggerAndWait then stayed waiting forever. TTL expiry had the same shape: its worker retry returned early on a non-pending run, and the batch expiry path swallowed a failed waitpoint-job enqueue.

Fix

Every finalizing path (attempt success, permanent failure, cancellation, TTL expiry) now enqueues a durable ensureRunFinalized job before the finish commit, and acks it once the inline side effects all succeed. In steady state the guard never executes; the cost is one Redis enqueue and ack per completion.

When the inline path dies in between, the guard fires after a short delay and re-derives everything from current state: it releases the run's queue message and concurrency slot, completes a still-pending associated waitpoint from the run row's output or error, re-runs the blocked-run fan-out (covering a lost unblock enqueue even after the waitpoint committed), and re-schedules the batch completion check. Every leg is idempotent, so racing the inline path is a no-op. The job retries with a capped backoff for roughly five weeks before dead-lettering, so it outlives any database outage while a genuinely poisoned item still becomes visible.

Cancellation gets special handling: CANCELED is the only terminal run status where execution can still be in flight, so the guard only re-delivers for a canceled run once its execution snapshot is FINISHED, re-arming itself until then rather than resuming the parent while the child is still winding down.

A finalization_rederivations counter increments whenever the guard actually re-delivers a lost signal; it should stay at zero in a healthy system.

Tests cover six shapes: waitpoint completion lost after the finish commit, unblock fan-out lost after the waitpoint completed, a failed guard enqueue failing the completion request with nothing committed, a stale guard held back during an in-flight cancellation, waitpoint completion lost during TTL expiry, and the happy path where the guard is acked and never runs.

Known accepted edge: a guard re-run after a partial inline completion can re-emit a cached-run completion event for the same span; this only happens during failure recovery and is bounded to duplicate trace events.

…gnals

A database error between a run's finish commit and its waitpoint
completion could permanently strand a parent blocked on triggerAndWait
or batchTriggerAndWait: the child's retries hit the already-finished
guard and nothing ever re-delivered the completion signal.

Every finalizing path now enqueues a durable ensureRunFinalized job
before the finish commit and acks it once the inline side effects
(waitpoint completion, unblock fan-out, batch nudge) all succeed. If
the inline path dies in between, the guard re-derives the lost signal
from the run row. All legs are idempotent, and the job never
dead-letters so it outlives any database outage.
@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 933a6a6

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 Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b23a596-d7b4-447f-90e0-0063934d67de

📥 Commits

Reviewing files that changed from the base of the PR and between 5c1dcdb and 116abe2.

📒 Files selected for processing (4)
  • internal-packages/run-engine/src/engine/index.ts
  • internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts
  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
  • internal-packages/run-engine/src/engine/workerCatalog.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (33)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (9)
We use vitest exclusively. **Never mock anything** - use testcontainers instead.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
  • internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts
  • internal-packages/run-engine/src/engine/workerCatalog.ts
  • internal-packages/run-engine/src/engine/index.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
  • internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts
  • internal-packages/run-engine/src/engine/workerCatalog.ts
  • internal-packages/run-engine/src/engine/index.ts
Implement tests for RunEngine in `src/engine/tests/` using testcontainers for Redis and PostgreSQL containerization

📄 CodeRabbit inference engine (internal-packages/run-engine/CLAUDE.md)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
Integrate OpenTelemetry tracer and meter instrumentation in RunEngine systems for observability

📄 CodeRabbit inference engine (internal-packages/run-engine/CLAUDE.md)

Files:

  • internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts
Use vitest for all tests in the Trigger.dev repository

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
  • internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts
  • internal-packages/run-engine/src/engine/workerCatalog.ts
  • internal-packages/run-engine/src/engine/index.ts
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
  • internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts
  • internal-packages/run-engine/src/engine/workerCatalog.ts
  • internal-packages/run-engine/src/engine/index.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts
  • internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts
  • internal-packages/run-engine/src/engine/workerCatalog.ts
  • internal-packages/run-engine/src/engine/index.ts
🔇 Additional comments (4)
internal-packages/run-engine/src/engine/workerCatalog.ts (1)

92-97: LGTM!

internal-packages/run-engine/src/engine/index.ts (1)

315-315: LGTM!

Also applies to: 431-431, 515-515

internal-packages/run-engine/src/engine/tests/ensureRunFinalized.test.ts (1)

391-450: LGTM!

internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts (1)

1907-1907: 🩺 Stability & Availability

No change needed. Worker.processItem acknowledges with the current deduplicationKey, and ackItem preserves a re-armed item with a new key. The old delivery acknowledgement cannot remove the newly scheduled guard.


Walkthrough

Adds a delayed ensureRunFinalized worker guard for successful, canceled, permanently failed, and expired runs. The guard re-delivers waitpoint completion and batch unblocking when inline finalization side effects fail. The engine exposes a configurable guard delay with a 60-second default. Worker registration, TTL scheduling, and unblock routing are added. Integration tests cover failed side effects, cancellation ordering, enqueue failures, TTL expiration, and successful inline finalization.

Merge Risk: 🟡 Moderate · up to 116ab

Terminal-run recovery can still resume dependent workflows before canceled execution has fully stopped, while a runtime-environment lookup failure can leave queue or concurrency capacity unreleased. These bounded failure paths can cause inconsistent workflow behavior or reduced availability and require explicit owner acceptance or fixes before merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a detailed and relevant summary, fix explanation, test coverage, and known edge case, but it does not follow the repository template. It omits the issue reference, checklist, … Add the required template sections. Include the issue number after "Closes #", complete the checklist, document the exact testing steps and commands, add a short changelog entry, and include screenshots or state that screenshots are not app…
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the run-engine and webapp fix and accurately summarizes the durable guard for lost run-finalization signals.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description provides a detailed and relevant summary, fix explanation, test coverage, and known edge case, but it does not follow the repository template. It omits the issue reference, checklist, explicit Testing section with test steps, Changelog section, and Screenshots section.

Resolution

Add the required template sections. Include the issue number after "Closes #", complete the checklist, document the exact testing steps and commands, add a short changelog entry, and include screenshots or state that screenshots are not applicable.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/run-finalization-guard-tri-13537

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

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

…queue failure

A guard surviving a failed finish commit could fire after a later
cancellation while the worker was still executing in PENDING_CANCEL,
resuming the parent before execution actually stopped. CANCELED is the
one terminal status where execution may still be in flight, so the
guard now re-delivers for a canceled run only once its latest snapshot
is FINISHED; the cancellation finalize path owns the window before
that.

Also documents the real dead-letter behavior (about five weeks of
retries at the capped backoff, then DLQ for visibility) and adds tests
for the failed-guard-enqueue and cancel-race shapes.
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Three hardening changes to the run finalization guard:

The cancel path enqueued the guard after the CANCELED status commit,
so a crash between the two could still strand a waiting parent. The
enqueue now happens before the status write, restoring the write-ahead
invariant on every finalizing path.

A guard that finds a canceled run whose execution has not reached
FINISHED now re-arms itself instead of exiting, so the watcher
survives the PENDING_CANCEL window without resuming the parent early.

The guard now also re-delivers the queue ack, releasing the queue
message and concurrency slot when the inline path died before
acknowledging, and TTL expiry gets the same guard: both the per-run
path (whose worker retry was defeated by the not-PENDING early return)
and the batch path (which swallowed a failed waitpoint-job enqueue)
now arm it before the EXPIRED commit.
coderabbitai[bot]

This comment was marked as resolved.

The CANCELED gate treated any non-FINISHED snapshot as a cancellation
in flight, but only the executing, pending-executing and pending-cancel
states carry heartbeats that force the finalize path. A canceled run
crashed while queued, delayed, created or suspended has nobody left to
produce a FINISHED snapshot, so the guard re-armed forever and the
waiting parent stayed stranded.

The deferral now matches the worker-owned states exactly and the guard
delivers in every other snapshot state. Adds a crash-recovery test for
a queued child canceled just before its FINISHED snapshot.
coderabbitai[bot]

This comment was marked as resolved.

The guard's watch on an in-flight cancellation relied on the snapshot
heartbeat to force the finalize path, but the heartbeat enqueue is a
separate write after the snapshot commit and can itself be lost,
leaving the guard deferring forever. The deferral now carries a
counter and delivers after ten cycles: resuming the parent with the
identical cancellation error beats an invisible infinite watch, and
the budget exhaustion is logged and counted.
devin-ai-integration[bot]

This comment was marked as resolved.

@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 1 new potential issue.

Devin Review

Comment thread .server-changes/run-finalization-guard.md
@matt-aitken
matt-aitken merged commit ef7b3aa into main Aug 31, 2026
52 checks passed
@matt-aitken
matt-aitken deleted the fix/run-finalization-guard-tri-13537 branch August 31, 2026 16:34
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.

2 participants