Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Concurrency and locking

When several processes run migrations against the same server database at once (a common case: multiple app instances applying migrations on boot), they must not race. migrant serializes them with a database advisory lock.

What happens

For a run against PostgreSQL or MySQL, migrant takes a session-level advisory lock that it holds for the whole run:

  • PostgreSQL: pg_advisory_lock.
  • MySQL: GET_LOCK (blocks until the lock is available).

A second migrator blocks until the first finishes, then proceeds. The lock is released when the run ends, and automatically by the database if the connection (session) drops, so a crashed migrator cannot leave a stuck lock.

After taking the lock, migrant re-reads the applied migrations. A run that waited for a peer therefore observes the migrations that peer committed and does not re-apply them.

SQLite has no advisory lock and no cross-process migration concurrency (a single connection already serializes writers), so locking is a no-op there.

Interaction with --force

migrant apply --force continues past a failed migration. On a server database a failed statement is recovered in place (a rollback) rather than by dropping the connection, so the session, and the advisory lock it holds, survives the error. A --force run keeps holding the lock as it continues, with no window for another migrator to interleave.

The one case where the lock is unavoidably lost mid-run is a genuinely dead connection: if the session ends, the database has already released the lock. A synchronized run detects this and aborts with an error rather than continuing unserialized on a new, unlocked session. Re-run migrations to continue.

Library control

Runs are synchronized by default. The Migrator::synchronized(bool) builder toggles it, for example when an outer mechanism already serializes migrations:

#![allow(unused)]
fn main() {
Migrator::with_config(&config)
    .synchronized(false) // opt out of the advisory lock
    .all(true)
    .apply()?;
}

The migrant CLI exposes the same toggle as --no-sync on apply and redo, for callers that serialize migrations externally; see The CLI.

See Using migrant_lib for the rest of the Migrator API.