Uh oh!
There was an error while loading. Please reload this page.
fix(db): serialize concurrent migrations with a Postgres advisory lock - #4939
Conversation
Deployments start N app replicas at once, each with a migration sidecar. drizzle migrate() has no cross-process lock, so all N read __drizzle_migrations, all see the same migration pending, and all apply it concurrently — one wins, the losers run the same DDL against already-mutated state and exit 1 (e.g. DROP TABLE "form" -> table does not exist / TaskFailedToStart). Wrap migrate() in a session-level pg_advisory_lock so runners serialize: the winner migrates, the losers block, then re-read and find nothing pending. Session locks auto-release on disconnect, so a crashed runner never wedges the lock.
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview The migrate script now acquires a session-level Reviewed by Cursor Bugbot for commit 34980b0. Configure here. |
Greptile SummaryThis PR wraps drizzle's
Confidence Score: 5/5Safe to merge — the advisory lock correctly serializes concurrent migration sidecars, unlock failures are safely swallowed, and the session-level lock auto-releases on disconnect. The change is a focused, single-file fix. The lock acquisition and release are correctly scoped, the nested try/finally prevents unlock errors from corrupting the exit code, and the BigInt key is within PostgreSQL's bigint range. No data-correctness or reliability issues were identified. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant R1 as Runner 1 (winner)
participant R2 as Runner 2 (loser)
participant PG as PostgreSQL
par Concurrent start
R1->>PG: "SET statement_timeout = 0"
R2->>PG: "SET statement_timeout = 0"
end
R1->>PG: pg_advisory_lock(MIGRATION_LOCK_KEY)
PG-->>R1: acquired ✓
R2->>PG: pg_advisory_lock(MIGRATION_LOCK_KEY)
Note over R2,PG: blocks — waits for R1
R1->>PG: migrate() — applies pending DDL
PG-->>R1: committed ✓
R1->>PG: pg_advisory_unlock(MIGRATION_LOCK_KEY)
PG-->>R1: released ✓
R1->>R1: process.exit(0)
PG-->>R2: lock acquired ✓
R2->>PG: migrate() — reads __drizzle_migrations, nothing pending
PG-->>R2: no-op ✓
R2->>PG: pg_advisory_unlock(MIGRATION_LOCK_KEY)
R2->>R2: process.exit(0)
Reviews (2): Last reviewed commit: "refactor(db): move unlock-guard rational..." | Re-trigger Greptile |
Uh oh!
There was an error while loading. Please reload this page.
…ation If the explicit unlock throws (e.g. connection drops in the window after migrate() commits), the exception bubbled to the outer catch and exited 1 — falsely reporting a failed migration to the deploy orchestrator. The session lock auto-releases on disconnect anyway, so swallow and log instead.
waleedlatif1
commented
Jun 10, 2026
waleedlatif1
commented
Jun 10, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 34980b0. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
Summary
migrate()in a session-levelpg_advisory_lockso concurrent migration sidecars serialize instead of racingDROP TABLE "form"→table "form" does not exist/ TaskFailedToStart)__drizzle_migrations, find nothing pending, and exit cleanly. Session locks auto-release on disconnect, so a crashed runner never wedges the lockType of Change
Testing
Tested manually — typecheck + lint pass. The current prod DB already has the migration applied (the race self-resolved on retry); this prevents recurrence on the next migration-bearing deploy.
Checklist