Skip to content

fix(core): enforce maxElapsed against wall-clock time in ExponentialB…#3779

Closed
singhharsh1708 wants to merge 1 commit into
triggerdotdev:mainfrom
singhharsh1708:fix/backoff-maxelapsed-wallclock
Closed

fix(core): enforce maxElapsed against wall-clock time in ExponentialB…#3779
singhharsh1708 wants to merge 1 commit into
triggerdotdev:mainfrom
singhharsh1708:fix/backoff-maxelapsed-wallclock

Conversation

@singhharsh1708
Copy link
Copy Markdown

Closes #3726

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

[Describe the steps you took to test this change]


Changelog

[Short description of what has changed]


Screenshots

[Screenshots]

💯

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: 09b5c46

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 32 packages
Name Type
@trigger.dev/core Patch
@trigger.dev/build Patch
trigger.dev Patch
@trigger.dev/plugins Patch
@trigger.dev/python Patch
@trigger.dev/redis-worker Patch
@trigger.dev/schema-to-json Patch
@trigger.dev/sdk Patch
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@trigger.dev/rbac Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/schedule-engine Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/tsql Patch
@internal/zod-worker Patch
references-ai-chat Patch
d3-chat Patch
references-d3-openai-agents Patch
references-nextjs-realtime Patch
references-realtime-hooks-test Patch
references-realtime-streams Patch
references-telemetry Patch
@internal/sdk-compat-tests Patch
@trigger.dev/react-hooks Patch
@trigger.dev/rsc Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions
Copy link
Copy Markdown
Contributor

Hi @singhharsh1708, 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 May 29, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e88e3f34-f95c-40a5-b6ab-80548c342687

📥 Commits

Reviewing files that changed from the base of the PR and between 9cb6fd1 and 09b5c46.

📒 Files selected for processing (3)
  • .changeset/backoff-maxelapsed-wallclock.md
  • packages/core/src/v3/apps/backoff.test.ts
  • packages/core/src/v3/apps/backoff.ts

Walkthrough

This PR fixes ExponentialBackoff.execute() to honor the maxElapsed wall-clock timeout by tracking actual elapsed time including callback execution duration. The implementation adds a check in the retry loop's finally block to break when total elapsed time reaches maxElapsed * 1000 milliseconds. The change includes three new test cases validating wall-clock timeout enforcement, successful callback returns after failures, and maxRetries limit handling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown
Contributor

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

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.

View 2 additional findings in Devin Review.

Open in Devin Review

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.

🟡 maxElapsed wall-clock check bypassed when AttemptTimeout fires due to continue

The new maxElapsed wall-clock check at line 344 is placed after the try/catch/finally block. When an AttemptTimeout is caught at line 336-337, the continue statement jumps to the next loop iteration, skipping the maxElapsed check entirely. While the finally block at line 339 correctly updates elapsedMs, the break condition at line 344 is never evaluated for timeout iterations.

This means that if a caller passes a non-zero attemptTimeoutMs, and callbacks keep timing out, the loop will ignore the maxElapsed boundary and continue retrying until maxRetries is exhausted or the iterator's own delay-based elapsed check (packages/core/src/v3/apps/backoff.ts:130) triggers — which only accounts for computed delay values, not real wall-clock time spent in timed-out callbacks.

Fix: move the check before continue or into finally

The maxElapsed check should be evaluated before the continue statement, e.g.:

if (error instanceof AttemptTimeout) {
  if (elapsedMs >= this.#maxElapsed * 1000) {
    break;
  }
  continue;
}

However, elapsedMs is only updated in the finally block which runs after the catch block, so the check would need to be restructured — either by moving the elapsed calculation before the check, or by placing the maxElapsed check inside the finally block.

(Refers to lines 336-338)

Prompt for agents
In packages/core/src/v3/apps/backoff.ts, the execute() method has a new maxElapsed wall-clock check at line 344 that is placed after the try/catch/finally block. However, the AttemptTimeout handler at line 336-338 uses 'continue' which skips past this check. The finally block at line 339-342 does correctly update elapsedMs before continue takes effect, but the break-check at line 344 is never reached for timeout iterations.

The fix requires restructuring: the maxElapsed check needs to happen AFTER elapsedMs is updated (in finally) but BEFORE continue skips it. Options:
1. Move the maxElapsed check into the finally block (but be careful: finally also runs on success/StopRetrying returns)
2. Update elapsedMs before the continue, then check maxElapsed, something like: compute elapsed in the catch block before continue, then check.
3. Use a flag set in finally and check it at the top of the next iteration.

The cleanest approach is probably to restructure the catch block for AttemptTimeout to not use continue — instead let it fall through to the maxElapsed check like normal errors do.
Open in Devin Review

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

Comment on lines +344 to +346
if (elapsedMs >= this.#maxElapsed * 1000) {
break;
}
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.

🚩 When maxElapsed is exceeded, cause is reported as "MaxRetries" which is semantically inaccurate

When the new wall-clock maxElapsed check triggers a break at line 345, the code falls through to line 354-359 which returns cause: "MaxRetries". This is semantically incorrect — the loop stopped because of elapsed time, not because retries were exhausted. The return type union at line 289 ("StopRetrying" | "Timeout" | "MaxRetries") doesn't include a "MaxElapsed" variant. The test at packages/core/src/v3/apps/backoff.test.ts:67 explicitly asserts cause: "MaxRetries" for the maxElapsed case, so this appears intentional to avoid a breaking change to the return type. However, callers who rely on cause to distinguish between these two exit conditions won't be able to tell them apart. This is a design trade-off rather than a bug, but worth considering adding a "MaxElapsed" cause in a future minor version.

Open in 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(v3): maxElapsed timeout boundary bypassed in ExponentialBackoff.execute

1 participant