Skip to content

feat: migrate Prisma from 6.14.0 to 7.7.0 with driver adapters - #4469

Closed
deepshekhardas wants to merge 20 commits into
triggerdotdev:mainfrom
deepshekhardas:feat/prisma-7-upgrade
Closed

feat: migrate Prisma from 6.14.0 to 7.7.0 with driver adapters#4469
deepshekhardas wants to merge 20 commits into
triggerdotdev:mainfrom
deepshekhardas:feat/prisma-7-upgrade

Conversation

@deepshekhardas

Copy link
Copy Markdown

Migrates Prisma to 7.7.0 with driver adapters. See original PR #3391.

Deploy Bot and others added 20 commits February 2, 2026 16:16
- Include reproduction scripts for Sentry (triggerdotdev#2900) and engine strictness (triggerdotdev#2913)
- Include PR body drafts for consolidated tracking
- Include reproduction scripts for Sentry (triggerdotdev#2900) and engine strictness (triggerdotdev#2913)
- Include PR body drafts for consolidated tracking
When the underlying logical-replication client errored (e.g. after a
Postgres failover), the runs and sessions replication services logged
the error and left the stream stopped. The host process kept running,
the WAL backed up, and ClickHouse silently fell behind.

Both services now run a configurable recovery strategy on stream errors,
defaulting to in-process reconnect with exponential backoff so a fresh
self-hosted setup heals on its own:

- "reconnect" (default) re-subscribes via the existing subscribe(lastLsn)
  path with exponential backoff (1s -> 60s cap, unlimited attempts), which
  re-validates the publication, re-acquires the leader lock, and resumes
  from the last acknowledged LSN.
- "exit" calls process.exit after a short flush window so a host's
  supervisor (Docker restart=always, systemd, k8s, etc.) can replace the
  process.
- "log" preserves the historical behaviour.

Per-service strategy + exit knobs are env-driven via
RUN_REPLICATION_ERROR_STRATEGY / SESSION_REPLICATION_ERROR_STRATEGY plus
matching *_EXIT_DELAY_MS / *_EXIT_CODE. Reconnect tuning is shared
across both services via REPLICATION_RECONNECT_INITIAL_DELAY_MS /
_MAX_DELAY_MS / _MAX_ATTEMPTS (0 = unlimited).
Addresses PR review feedback:

- LogicalReplicationClient.subscribe() can throw before its internal
  "error" listener is wired up (notably when pg client.connect() fails
  mid-failover). The reconnect strategy's catch block only logged, so
  recovery silently stopped. Now also calls scheduleReconnect(err) — the
  pendingReconnect guard makes it idempotent if an error event was also
  emitted.
- Reject negative values for the new replication-recovery env vars and
  cap exit codes at 255.
- Convert the new ReplicationErrorRecovery{Deps,} interfaces to type
  aliases to match the repo's TypeScript style.
- Tighten the reconnect dep comment to drop a stale "lastAcknowledgedLsn"
  reference (the wrapper-tracked resume LSN is what callers actually pass).
- Restore process.exit after service.shutdown() in the exit-strategy
  test so a delayed exit timer can't terminate the test worker.
LogicalReplicationClient.subscribe() can resolve without throwing or
emitting an "error" event when leader-lock acquisition fails — it just
calls this.stop() and returns. The reconnect callback now checks
isStopped after subscribe() and throws so the recovery handler can
schedule the next attempt instead of silently giving up.
…rough handle()

The previous post-subscribe() isStopped check was always true on the
happy path: subscribe() calls stop() up front (setting _isStopped=true)
and only resets the flag inside the replicationStart event, which fires
asynchronously after subscribe() returns. So the check threw on every
successful reconnect, the catch rescheduled, the next attempt tore down
the just-built client, and the cycle continued — replication briefly
worked between teardowns, which is why the integration test passed.

Replace it with the correct nudge: subscribe to leaderElection and call
the recovery handler on isLeader=false. That's the only subscribe()
exit path that doesn't either throw or emit an "error" event (the other
silent-return paths emit "error" first via createPublication/createSlot
failures).
The previous commit routed leaderElection(false) through handle(), which
under the exit strategy schedules process.exit. In a multi-instance
deployment that turns lost leader election — a normal operational state
— into a restart loop: exit, supervisor restarts, election fails again,
exit, and so on.

Add a dedicated notifyLeaderElectionLost() on ReplicationErrorRecovery
that the reconnect strategy treats as another retry trigger, while
exit and log strategies no-op. Wire the wrapper services through the
new method.
fix(webapp): auto-recover replication services after stream errors
…iggerdotdev#3391)

- Update schema.prisma for Prisma 7 compatibility
- Add prisma.config.ts with driver adapter setup
- Update transaction.ts for new Prisma client API
- Update docker/entrypoint.sh and Dockerfile for pnpm 10.23.0
- Update package.json across packages for Prisma 7 deps
- Update testcontainers, run-engine tests for new Prisma version
- Add references/prisma-7/package.json placeholder

Closes triggerdotdev#3391
@changeset-bot

changeset-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5f4c41a

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

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

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@deepshekhardas, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c9e4339e-dd8e-415b-8d92-0b0e7e25166a

📥 Commits

Reviewing files that changed from the base of the PR and between 14824b0 and 5f4c41a.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • references/prisma-7/package.json is excluded by !references/**
📒 Files selected for processing (47)
  • .changeset/fix-console-interceptor-2900.md
  • .changeset/fix-docker-hub-rate-limit-2911.md
  • .changeset/fix-github-install-node-version-2913.md
  • .changeset/fix-orphaned-workers-2909.md
  • .changeset/fix-sentry-oom-2920.md
  • .server-changes/replication-error-recovery.md
  • apps/webapp/app/db.server.ts
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/routes/metrics.ts
  • apps/webapp/app/services/replicationErrorRecovery.server.ts
  • apps/webapp/app/services/runsReplicationInstance.server.ts
  • apps/webapp/app/services/runsReplicationService.server.ts
  • apps/webapp/app/services/sessionsReplicationInstance.server.ts
  • apps/webapp/app/services/sessionsReplicationService.server.ts
  • apps/webapp/app/v3/tracer.server.ts
  • apps/webapp/package.json
  • apps/webapp/test/runsReplicationBenchmark.producer.ts
  • apps/webapp/test/runsReplicationService.errorRecovery.test.ts
  • consolidated_pr_body.md
  • docker/Dockerfile
  • docker/scripts/entrypoint.sh
  • internal-packages/database/package.json
  • internal-packages/database/prisma.config.ts
  • internal-packages/database/prisma/schema.prisma
  • internal-packages/database/src/transaction.ts
  • internal-packages/database/tsconfig.json
  • internal-packages/run-engine/src/engine/tests/priority.test.ts
  • internal-packages/run-engine/src/engine/tests/waitpoints.test.ts
  • internal-packages/testcontainers/package.json
  • internal-packages/testcontainers/src/index.ts
  • internal-packages/testcontainers/src/utils.ts
  • packages/cli-v3/src/cli/common.ts
  • packages/cli-v3/src/commands/deploy.ts
  • packages/cli-v3/src/commands/dev.ts
  • packages/cli-v3/src/commands/login.ts
  • packages/cli-v3/src/commands/update.test.ts
  • packages/cli-v3/src/commands/update.ts
  • packages/cli-v3/src/deploy/buildImage.ts
  • packages/cli-v3/src/entryPoints/dev-index-worker.ts
  • packages/cli-v3/src/entryPoints/dev-run-worker.ts
  • packages/cli-v3/src/entryPoints/managed-index-worker.ts
  • packages/cli-v3/src/entryPoints/managed-run-worker.ts
  • packages/cli-v3/src/utilities/sourceMaps.test.ts
  • packages/cli-v3/src/utilities/sourceMaps.ts
  • packages/core/src/v3/consoleInterceptor.ts
  • scripts/recover-stuck-runs.ts
  • tests/utils.ts

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.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Hi @deepshekhardas, 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 Aug 2, 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 12 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines 214 to +217
} finally {
await watcher?.stop();
process.off("SIGINT", signalHandler);
process.off("SIGTERM", signalHandler);
await cleanup();

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.

🔴 Local development mode shuts itself down immediately after starting

The whole local dev session is torn down (await cleanup() at packages/cli-v3/src/commands/dev.ts:217) as soon as start-up finishes, because the call it waits on returns instantly, so running trigger dev kills the task runtime and deletes its lock file right after launch.
Impact: Developers can no longer run tasks locally — the dev session stops working the moment it starts.

Why the wait returns immediately and what cleanup destroys

startDev returns waitUntilExit defined as const waitUntilExit = async () => { }; (packages/cli-v3/src/commands/dev.ts:312), so await devInstance.waitUntilExit() resolves on the next tick and the finally block runs immediately.

Previously the finally only did await watcher?.stop() (stopping the config file watcher), which left the dev session (bundler watch, runtime, MCP server) running. Now cleanup() calls devInstance.stop(), which is startDev's stop: it calls devInstance?.stop() on the DevSessionInstancedestination.remove(), stopBundling(), runtime.shutdown(), stopOutput(), stopMcpServer() (packages/cli-v3/src/dev/devSession.ts:247-255) — plus watcher.stop() and removeLockFile().

The cleanup should only run on actual exit (e.g. from the signal handler), not right after start-up; waitUntilExit needs to return a promise that only resolves when the session actually ends.

Prompt for agents
In packages/cli-v3/src/commands/dev.ts, devCommand now runs `await cleanup()` in a finally block after `await devInstance.waitUntilExit()`. But startDev returns `waitUntilExit` as a no-op async function that resolves immediately, so the finally block executes right after start-up and calls devInstance.stop(), which tears down the DevSessionInstance (destination.remove, stopBundling, runtime.shutdown, stopOutput, stopMcpServer in packages/cli-v3/src/dev/devSession.ts) and removes the lockfile. Before this PR the finally only stopped the config watcher, so the session stayed alive. Fix by making waitUntilExit resolve only when the dev session actually ends (e.g. a promise resolved by the signal handler / session teardown), or by not invoking full cleanup on the normal return path.
Open in Devin Review

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

Comment on lines +209 to +210
process.on("SIGINT", signalHandler);
process.on("SIGTERM", signalHandler);

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.

🟡 Ctrl+C cleanup during local development never runs because an earlier handler exits first

The new interrupt cleanup step is registered after a pre-existing handler that quits the program instantly (process.on("SIGINT", signalHandler) at packages/cli-v3/src/commands/dev.ts:209), so pressing Ctrl+C still terminates before any child processes are cleaned up.
Impact: Orphaned worker processes and stale lock files can still be left behind after interrupting local development.

Handler ordering

installExitHandler() runs at module load in packages/cli-v3/src/cli/index.ts:46 and registers process.on("SIGINT", () => process.exit(0)) (packages/cli-v3/src/cli/common.ts:88-95). Node invokes signal listeners in registration order, and that first listener calls process.exit(0) synchronously, so the async signalHandler added later in devCommand never gets a chance to await cleanup().

A fix would be to make the global exit handler cooperative (e.g. allow commands to register async shutdown steps) or to remove/override the global SIGINT/SIGTERM listeners while the dev command is running.

Open in Devin Review

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

export const UpdateCommandOptions = CommonCommandOptions.pick({
logLevel: true,
skipTelemetry: true,
ignoreEngines: true,

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.

🟡 Option to skip Node version checks during deployment install has no effect

The new setting for skipping Node engine checks is accepted and forwarded (ignoreEngines: true at packages/cli-v3/src/commands/update.ts:21 and packages/cli-v3/src/commands/deploy.ts:262) but the install step never uses it, so deployments still fail when the project declares a stricter Node version.
Impact: The advertised fix for build-server deployment failures on Node version mismatch does not work, and the newly added tests for it fail.

Missing plumbing into installDependencies

updateTriggerPackages still calls await installDependencies({ cwd: projectPath, silent: true }); (packages/cli-v3/src/commands/update.ts:260) with no args. The new test file packages/cli-v3/src/commands/update.test.ts:74-112 asserts installDependencies is called with args: ["--no-engine-strict"] (npm), ["--config.engine-strict=false"] (pnpm), ["--ignore-engines"] (yarn) and [] otherwise — none of which can pass against the current implementation.

The fix is to derive the package-manager-specific flag from options.ignoreEngines and pass it as args to installDependencies.

Prompt for agents
packages/cli-v3/src/commands/update.ts now accepts an `ignoreEngines` option (added to UpdateCommandOptions and passed from deploy.ts), but updateTriggerPackages never uses it: it still calls installDependencies({ cwd, silent: true }) with no args. The new test packages/cli-v3/src/commands/update.test.ts expects package-manager-specific flags to be forwarded (npm: --no-engine-strict, pnpm: --config.engine-strict=false, yarn: --ignore-engines, otherwise []). Implement the mapping using the already-detected packageManager and pass the resulting array as `args` to installDependencies.
Open in Devin Review

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

"push",
"--force-reset",
"--accept-data-loss",
"--skip-generate",

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.

🟡 Container-based tests now regenerate the database client on every run

The flag that prevented client regeneration was dropped from the test database setup command ("--skip-generate" removed at internal-packages/testcontainers/src/utils.ts:32), so every test that starts a Postgres container rewrites the shared generated client directory while other tests are running.
Impact: Test runs get noticeably slower and can fail intermittently when parallel suites regenerate the same generated client files.

Details

createPostgresContainer runs prisma db push --force-reset --accept-data-loss --schema ... --url ... for each container fixture. Without --skip-generate, Prisma regenerates into internal-packages/database/generated/prisma, which is the exact directory already imported by the running test processes. Multiple suites executing container fixtures concurrently write to that directory simultaneously.

Unless Prisma 7 requires generation here (it does not — generation happens at build time), --skip-generate should be restored alongside the new --url argument.

Open in Devin Review

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

Comment on lines +8 to +9
url: process.env.DATABASE_URL ?? "postgresql://localhost:5432/trigger",
directUrl: process.env.DIRECT_URL,

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.

🟡 Database migrations silently fall back to a local database when the connection string is missing

The migration configuration substitutes a hard-coded local database address when no connection string is configured (process.env.DATABASE_URL ?? "postgresql://localhost:5432/trigger" at internal-packages/database/prisma.config.ts:8), so a misconfigured environment no longer fails loudly.
Impact: A deployment with a missing database setting will quietly try to migrate a wrong/local database instead of erroring out.

Behavior change

Previously the datasource block in internal-packages/database/prisma/schema.prisma used env("DATABASE_URL"), and Prisma raised an explicit "environment variable not found" error when it was unset. With the url moved into prisma.config.ts and defaulted, commands like prisma migrate deploy / db push / migrate reset (invoked from docker/scripts/entrypoint.sh) will target postgresql://localhost:5432/trigger instead. db:reset in particular is destructive.

Prefer throwing when DATABASE_URL is absent rather than defaulting.

Suggested change
url: process.env.DATABASE_URL ?? "postgresql://localhost:5432/trigger",
directUrl: process.env.DIRECT_URL,
url: process.env.DATABASE_URL ?? invariantDatabaseUrl(),
directUrl: process.env.DIRECT_URL,
Open in Devin Review

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

Comment on lines +110 to +111
const adapter = new PrismaPg(url);
const prisma = new PrismaClient({ adapter });

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.

🔍 Prisma 7 migration missed two remaining datasources client constructions

@trigger.dev/database now generates a Prisma 7 client with engineType = "client" and no url on the datasource, so new PrismaClient({ datasources: { db: { url } } }) no longer works. Two call sites were not migrated to PrismaPg:

  • internal-packages/testcontainers/src/webapp.ts:204
  • apps/webapp/test/helpers/sharedTestServer.ts:36

Both are test-only helpers (so not reported as bugs), but webapp integration tests that use the containerised webapp fixture will fail at runtime until they are converted the same way internal-packages/testcontainers/src/index.ts was.

Open in Devin Review

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

Comment on lines +146 to +152
{
// Generate deterministic prepared statement names from query SQL so PostgreSQL
// can reuse cached query plans. Without this, every query uses an anonymous
// prepared statement that PG must parse and plan from scratch each time.
statementNameGenerator: (query) => `p_${createHash("sha256").update(query.sql).digest("hex").slice(0, 16)}`,
}
);

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.

🔍 Deterministic prepared-statement names rely on SQL text alone

statementNameGenerator: (query) => \p_${sha256(query.sql).slice(0,16)}`derives the prepared-statement name only from the SQL text. node-postgres caches named statements per connection and reuses them when the name matches, so identical SQL is fine. The risk cases are (a) a 64-bit hash collision between two different statements on the same connection (negligible but non-zero), and (b) two logically distinct queries whose SQL text is identical but whose parameter types differ — Postgres would reject the second Parse with "prepared statement already exists" / type mismatch. Worth confirming against@prisma/adapter-pg@7.7.0` semantics (does the adapter guarantee the generator only needs uniqueness per distinct SQL+param-type tuple?) before running this in production, since a failure here surfaces as sporadic query errors rather than a startup failure.

Open in Devin Review

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

Comment on lines 15 to 20
const coreMetrics = await metricsRegister.metrics();

// Order matters, core metrics end with `# EOF`, prisma metrics don't
const metrics = prismaMetrics + coreMetrics;

return new Response(metrics, {
return new Response(coreMetrics, {
headers: {
"Content-Type": metricsRegister.contentType,
},

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.

🔍 Prisma pool/metric observability removed with no replacement

Dropping the metrics preview feature removes $metrics.json() / $metrics.prometheus(), which is why configurePrismaMetrics and the /metrics route composition were deleted. The consequence is that all connection-pool visibility (db.pool.connections.*, db.client.queries.*) disappears from both the OTel meter and the Prometheus scrape endpoint, right as the pool implementation changes from the Rust engine to node-postgres. Since pool sizing/idle behaviour is also being changed in this PR, it would be worth re-adding equivalent gauges from the pg.Pool instance (totalCount, idleCount, waitingCount) so regressions are observable.

(Refers to lines 15-21)

Open in Devin Review

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

Comment on lines +46 to +56
function isDriverAdapterTransactionWriteConflict(error: unknown): boolean {
if (typeof error !== "object" || error === null) return false;
const err = error as { name?: string; message?: string };
return err.name === "DriverAdapterError" && err.message === "TransactionWriteConflict";
}

export function isPrismaRetriableError(error: unknown): boolean {
if (isDriverAdapterTransactionWriteConflict(error)) {
return true;
}

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.

🔍 Driver-adapter write-conflict detection matches on error message text

isDriverAdapterTransactionWriteConflict matches err.name === "DriverAdapterError" && err.message === "TransactionWriteConflict". This is a string-equality check against an internal Prisma error shape, so any future wording change (or a wrapped/prefixed message) silently disables serialization-failure retries — a failure mode that only shows up as increased 40001 errors under load rather than a test failure. If DriverAdapterError exposes a structured cause/code (e.g. the PG SQLSTATE 40001), matching on that would be more durable. A unit test asserting the retry path against a synthesized error object would also help lock this in.

Open in Devin Review

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

Comment on lines +31 to +36
// Use a large debounce so the background processQueueForWorkerQueue job
// doesn't race with the manual processMasterQueueForEnvironment call.
// With PrismaPg adapter overhead each trigger() takes longer, so a small
// debounce causes items to be moved to the worker queue individually in
// arrival order rather than collectively in priority order.
processWorkerQueueDebounceMs: 10_000,

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.

🔍 Run-engine tests loosened with longer sleeps and a 10s debounce

Several timing assertions were relaxed (200ms → 1000ms sleeps, processWorkerQueueDebounceMs 50 → 10_000, idempotency TTL 200ms → 60s) with the rationale that PrismaPg adds per-query overhead. Two things to keep in mind: (a) the priority test now depends on the background worker-queue job never firing during the test rather than on ordering being correct, so it no longer exercises the debounced path at all; (b) the ~1s sleeps are absolute and will still be flaky on slower CI. If driver-adapter overhead is really large enough to change queue ordering semantics, that itself deserves a measurement before merging — it would affect production throughput too.

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.

2 participants