Skip to content

Acquire advisory lock around message database migrations#2523

Merged
jeremydmiller merged 1 commit intomainfrom
fix/migration-advisory-lock-2518
Apr 16, 2026
Merged

Acquire advisory lock around message database migrations#2523
jeremydmiller merged 1 commit intomainfrom
fix/migration-advisory-lock-2518

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Fixes #2518 — concurrent calls to MessageDatabase.MigrateAsync against a fresh schema race on CREATE SCHEMA IF NOT EXISTS and produce 23505 duplicate-key errors. Wolverine's previous behavior was a generic catch-all retry with random backoff that masked the race rather than preventing it.

Approach

Mirror Marten's pattern: acquire a session-scoped advisory lock around the migration, run the DDL, release the lock. The new DatabaseSettings.MigrationLockId (default 4006) is configurable so it can be aligned with Marten's StoreOptions.ApplyChangesLockId (default 4004) when both frameworks share a database via IntegrateWithWolverine.

Changes

  • DatabaseSettings.MigrationLockId — new configurable lock id (default 4006)
  • MessageDatabase<T>.ReleaseLockAsync — new virtual method (no-op default for SQLite)
  • PostgreSQL / SQL Server / MySQL providers — implement ReleaseLockAsync
  • MigrateAsync — bounded-retry lock acquisition (10 attempts, linear backoff up to 1s each), guaranteed release in finally, existing catch-all retry preserved as a backstop
  • PostgresqlBackedPersistence.OverrideMigrationLockId — fluent override to align with Marten
  • Tests — concurrent-migration test + direct lock-primitive verification in PostgresqlTests/Bugs/Bug_2518_*
  • Docs — updated docs/guide/durability/postgresql.md with alignment example

Out of scope

  • Automatic alignment with Marten's ApplyChangesLockId when IntegrateWithWolverine is active — would require coordination from Wolverine.Marten. Worth a follow-up issue.
  • Oracle (OracleMessageStore doesn't inherit from MessageDatabase<T> and has its own admin path)

Test plan

  • Bug_2518_concurrent_migration_advisory_lock.concurrent_migrate_async_calls_do_not_race_on_create_schema — 16 concurrent migrations against a fresh schema, all complete without errors
  • migration_lock_id_is_actually_held_during_migration — verifies the advisory lock primitive blocks a second session and is released cleanly
  • PostgresqlMessageStoreTests (38 tests) all still pass

🤖 Generated with Claude Code

Concurrent calls to MessageDatabase.MigrateAsync against a fresh schema
can race on CREATE SCHEMA IF NOT EXISTS and produce 23505 duplicate-key
errors against pg_namespace_nspname_index (or equivalents on other
engines). Wolverine's previous behavior was a generic catch-all retry
with random backoff — a band-aid that masked the race rather than
preventing it.

This change mirrors Marten's pattern: acquire a session-scoped advisory
lock around migration, run the DDL, then release. New
DatabaseSettings.MigrationLockId (default 4006) is configurable so it
can be aligned with Marten's StoreOptions.ApplyChangesLockId (default
4004) when both frameworks share a database via IntegrateWithWolverine.

- Add DatabaseSettings.MigrationLockId
- Add abstract ReleaseLockAsync to MessageDatabase<T> (virtual no-op default)
- Implement ReleaseLockAsync on PostgreSQL, SQL Server, MySQL providers
- Wrap MigrateAsync with bounded-retry lock acquisition + guaranteed release
- Add PostgresqlBackedPersistence.OverrideMigrationLockId fluent override
- Add concurrent-migration test plus a direct lock-primitive verification
- Document the new setting in postgresql.md

Closes #2518

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

Message Database Migration does not acquire Global Lock

1 participant