fix(ploeg): serialize schema migrations with an advisory lock #149

Merged
ryangr0 merged 1 commit from ryangr0/ploeg-migration-advisory-lock into development 2026-10-03 00:23:24 +00:00 AGit
Owner

The migration runner checked schema_migrations outside each migration's
transaction and held no lock across the loop. Two ploegd processes
starting together could both decide a migration was unapplied, and one
failed startup (in the regression test, already on the concurrent
CREATE TABLE IF NOT EXISTS schema_migrations).

Migrate now acquires one pool connection, takes a session
pg_advisory_lock keyed on hashtextextended('ploeg.schema-migrations', 0)
and runs the whole create-check-apply loop on that connection. The lock
is released in a deferred unlock that also runs on error, with a
context detached from cancellation; if the unlock fails the connection
is hijacked and closed so the pool never hands out a connection that
still holds the lock. A test runs two migrators concurrently against a
fresh database in the embedded PostgreSQL: both return without error,
each migration is recorded once and no advisory lock remains.

The new concurrent-migrator test failed 3/3 against the old code (duplicate pg_type from the racing CREATE TABLE) and passes 5/5 with the lock.

Verified with mise run verify on the pinned toolchain (all gates passed).

Ticket: https://vikunja.webgrip.dev/tasks/1726

🤖 Generated with Claude Code

The migration runner checked schema_migrations outside each migration's transaction and held no lock across the loop. Two ploegd processes starting together could both decide a migration was unapplied, and one failed startup (in the regression test, already on the concurrent CREATE TABLE IF NOT EXISTS schema_migrations). Migrate now acquires one pool connection, takes a session pg_advisory_lock keyed on hashtextextended('ploeg.schema-migrations', 0) and runs the whole create-check-apply loop on that connection. The lock is released in a deferred unlock that also runs on error, with a context detached from cancellation; if the unlock fails the connection is hijacked and closed so the pool never hands out a connection that still holds the lock. A test runs two migrators concurrently against a fresh database in the embedded PostgreSQL: both return without error, each migration is recorded once and no advisory lock remains. The new concurrent-migrator test failed 3/3 against the old code (duplicate pg_type from the racing CREATE TABLE) and passes 5/5 with the lock. Verified with `mise run verify` on the pinned toolchain (all gates passed). Ticket: https://vikunja.webgrip.dev/tasks/1726 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(ploeg): serialize schema migrations with an advisory lock
Some checks failed
[Workflow] On Pull Request / checks (pull_request) Has been cancelled
[Workflow] On Pull Request / warnings (pull_request) Has been cancelled
[Workflow] On Pull Request / release-policy (pull_request) Has been cancelled
68891e44ee
The migration runner checked schema_migrations outside each migration's
transaction and held no lock across the loop. Two ploegd processes
starting together could both decide a migration was unapplied, and one
failed startup (in the regression test, already on the concurrent
CREATE TABLE IF NOT EXISTS schema_migrations).

Migrate now acquires one pool connection, takes a session
pg_advisory_lock keyed on hashtextextended('ploeg.schema-migrations', 0)
and runs the whole create-check-apply loop on that connection. The lock
is released in a deferred unlock that also runs on error, with a
context detached from cancellation; if the unlock fails the connection
is hijacked and closed so the pool never hands out a connection that
still holds the lock. A test runs two migrators concurrently against a
fresh database in the embedded PostgreSQL: both return without error,
each migration is recorded once and no advisory lock remains.

VIK-1726

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ryangr0 merged commit a45390d005 into development 2026-10-03 00:23:24 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/unfold!149
No description provided.