-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(webapp,run-store,database): env-configurable transaction resilience (maxWait + tx-start retry) #4623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ericallam
merged 8 commits into
main
from
feature/tri-13295-make-transaction-resilience-knobs-maxwait-tx-start-retry-env
Aug 15, 2026
Merged
feat(webapp,run-store,database): env-configurable transaction resilience (maxWait + tx-start retry) #4623
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
99fac30
feat(webapp,run-store,database): env-configurable transaction resilie…
ericallam 451773f
refactor(webapp): route the $transaction helper to the passed client'…
ericallam dd41264
fix(database,webapp): address review — exclude acquisition errors fro…
ericallam 0e117c2
fix(webapp): move transaction-resilience config out of db.server so w…
ericallam c99e7bd
fix(database,webapp): make the retry kill switch a true revert, blank…
ericallam 1e4b80d
feat(webapp): default tx-start retry to 3 attempts (2 retries)
ericallam 9d2c26d
fix(webapp): address review — blank-tolerant retry-enable flag + per-…
ericallam e00cbeb
fix(database,run-store): retry driver-adapter acquire timeouts + gate…
ericallam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
.server-changes/transaction-resilience-during-db-interruptions.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: improvement | ||
| --- | ||
|
|
||
| Triggering tasks is now more resilient to brief, transient service interruptions, so short stalls are less likely to surface as errors. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { TokenBucketRetryBudget, type TransactionStartRetryConfig } from "@trigger.dev/database"; | ||
| import { env } from "~/env.server"; | ||
| import { logger } from "~/services/logger.server"; | ||
|
|
||
| /** | ||
| * Resolved transaction-resilience config for one writer pool. Each pool gets its own | ||
| * {@link TransactionStartRetryConfig} (with its OWN token bucket, so a storm on one pool cannot | ||
| * drain another's retry budget) plus the `maxWait` applied when that pool opens a transaction. | ||
| * Env is read here at the app boundary (IoC); the library never reads env. | ||
| * | ||
| * Kept out of `db.server` on purpose: `db.server` is mocked wholesale by ~150 tests, and a new | ||
| * export there breaks every mock that does not list it. Both `db.server` and `runStore.server` | ||
| * import these from here instead. | ||
| */ | ||
| export type TransactionResilienceConfig = { | ||
| maxWait: number; | ||
| startRetry: TransactionStartRetryConfig; | ||
| }; | ||
|
|
||
| function resolveTransactionResilience( | ||
| pool: "control-plane" | "run-ops" | "run-ops-legacy", | ||
| overrides: { | ||
| maxWaitMs?: number; | ||
| enabled?: boolean; | ||
| maxAttempts?: number; | ||
| backoffMinMs?: number; | ||
| backoffMaxMs?: number; | ||
| budgetPerSec?: number; | ||
| budgetBurst?: number; | ||
| } | ||
| ): TransactionResilienceConfig { | ||
| const budgetPerSec = | ||
| overrides.budgetPerSec ?? env.DATABASE_TRANSACTION_START_RETRY_BUDGET_PER_SEC; | ||
| const budgetBurst = overrides.budgetBurst ?? env.DATABASE_TRANSACTION_START_RETRY_BUDGET_BURST; | ||
| return { | ||
| maxWait: Math.max(0, overrides.maxWaitMs ?? env.DATABASE_TRANSACTION_MAX_WAIT_MS), | ||
| startRetry: { | ||
| options: { | ||
| enabled: overrides.enabled ?? env.DATABASE_TRANSACTION_START_RETRY_ENABLED, | ||
| maxAttempts: overrides.maxAttempts ?? env.DATABASE_TRANSACTION_START_RETRY_MAX_ATTEMPTS, | ||
| backoffMinMs: overrides.backoffMinMs ?? env.DATABASE_TRANSACTION_START_RETRY_BACKOFF_MIN_MS, | ||
| backoffMaxMs: overrides.backoffMaxMs ?? env.DATABASE_TRANSACTION_START_RETRY_BACKOFF_MAX_MS, | ||
| }, | ||
| budget: new TokenBucketRetryBudget({ ratePerSec: budgetPerSec, burst: budgetBurst }), | ||
| onRetry: ({ attempt, delayMs }) => | ||
| logger.warn("retrying transaction start after acquisition failure", { | ||
| pool, | ||
| attempt, | ||
| delayMs, | ||
| }), | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export const controlPlaneTransactionResilience = resolveTransactionResilience("control-plane", {}); | ||
|
|
||
| export const runOpsTransactionResilience = resolveTransactionResilience("run-ops", { | ||
| maxWaitMs: env.RUN_OPS_DATABASE_TRANSACTION_MAX_WAIT_MS, | ||
| enabled: env.RUN_OPS_DATABASE_TRANSACTION_START_RETRY_ENABLED, | ||
| maxAttempts: env.RUN_OPS_DATABASE_TRANSACTION_START_RETRY_MAX_ATTEMPTS, | ||
| backoffMinMs: env.RUN_OPS_DATABASE_TRANSACTION_START_RETRY_BACKOFF_MIN_MS, | ||
| backoffMaxMs: env.RUN_OPS_DATABASE_TRANSACTION_START_RETRY_BACKOFF_MAX_MS, | ||
| budgetPerSec: env.RUN_OPS_DATABASE_TRANSACTION_START_RETRY_BUDGET_PER_SEC, | ||
| budgetBurst: env.RUN_OPS_DATABASE_TRANSACTION_START_RETRY_BUDGET_BURST, | ||
| }); | ||
|
|
||
| export const runOpsLegacyTransactionResilience = resolveTransactionResilience("run-ops-legacy", { | ||
| maxWaitMs: env.RUN_OPS_LEGACY_DATABASE_TRANSACTION_MAX_WAIT_MS, | ||
| enabled: env.RUN_OPS_LEGACY_DATABASE_TRANSACTION_START_RETRY_ENABLED, | ||
| maxAttempts: env.RUN_OPS_LEGACY_DATABASE_TRANSACTION_START_RETRY_MAX_ATTEMPTS, | ||
| backoffMinMs: env.RUN_OPS_LEGACY_DATABASE_TRANSACTION_START_RETRY_BACKOFF_MIN_MS, | ||
| backoffMaxMs: env.RUN_OPS_LEGACY_DATABASE_TRANSACTION_START_RETRY_BACKOFF_MAX_MS, | ||
| budgetPerSec: env.RUN_OPS_LEGACY_DATABASE_TRANSACTION_START_RETRY_BUDGET_PER_SEC, | ||
| budgetBurst: env.RUN_OPS_LEGACY_DATABASE_TRANSACTION_START_RETRY_BUDGET_BURST, | ||
| }); | ||
|
|
||
| const transactionResilienceByClient = new WeakMap<object, TransactionResilienceConfig>(); | ||
|
|
||
| /** | ||
| * Associate a writer client with its pool's resilience config. Returns the client for inline use at | ||
| * construction. Kept here (not in db.server) so nothing new lands on db.server's wholesale-mocked | ||
| * export surface. | ||
| */ | ||
| export function registerTransactionResilience<T extends object>( | ||
| client: T, | ||
| resilience: TransactionResilienceConfig | ||
| ): T { | ||
| transactionResilienceByClient.set(client, resilience); | ||
| return client; | ||
| } | ||
|
|
||
| /** | ||
| * The resilience config registered for a writer client, or the control-plane config as a safe | ||
| * fallback. Derives resilience from the ACTUAL client identity rather than an assumed routing role, | ||
| * so run-ops clients aliased onto the control-plane pool (split flag off) correctly get the | ||
| * control-plane config instead of a run-ops override. | ||
| */ | ||
| export function resilienceForClient(client: object): TransactionResilienceConfig { | ||
| return transactionResilienceByClient.get(client) ?? controlPlaneTransactionResilience; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.