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 |
…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.
|
@greptile |
|
@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.
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