A migration locked my table and the deploy is waitingSolved

Help & QuestionsEric3 replies

Jump to the accepted answer

EricMember#1

A migration is running against a table with about four million rows, and it is holding a lock. The application is still serving reads, but writes are queuing and the deploy is stuck waiting for the migration to finish.

The lock is the "ACCESS EXCLUSIVE" kind that an ALTER TABLE takes, so nothing else can touch the table while it is there. I did not add a timeout, and I did not check what the table was doing before the migration started.

What do I do now, and what do I do differently?

3 replies

Devn NieMember#2

Accepted answer

Two questions decide it: is the migration making progress, and is it going to finish before the outage does?

Look at it. pg_stat_activity shows what every session is doing, and for the running migration you can see the query, the state and how long it has been running. pg_locks shows who is holding what and who is waiting.

If it is progressing, let it finish. Killing a migration that is halfway through a table rewrite leaves the table exactly as it was, plus the time already spent, plus a rollback to plan. Interrupting is the expensive option.

If it is not progressing, it is waiting for something, and the thing it is waiting for is almost always a long-running query that started before the migration and is holding a row lock. Find it in pg_stat_activity — look for idle in transaction and for long-running SELECTs — and deal with that one. Killing the migration while a blocking session holds the lock just moves the queue.

The immediate mitigation is to stop the application from queueing writes: put it in read-only mode if it has that, or scale the writers down. A queue that is not draining is a queue that will still be there after the migration finishes, and the timeouts you get in the meantime are the outage.

For next time, the three things that would have prevented this.

  1. Set a lock_timeout on the migration, so it gives up rather than queueing behind a long transaction. A migration that fails fast and can be retried is much better than one that waits silently.
  2. Do not add a column with a volatile default on a table that big. Postgres 11 and later can add a column with a non-volatile default without rewriting the table; a volatile default rewrites it, and the rewrite takes an exclusive lock for the whole duration. This is the single most common cause of exactly this situation.
  3. Add the column without a default, then backfill it in batches — a few thousand rows at a time, each batch in its own transaction, with a sleep between them. No long lock, and the backfill is restartable.

That last pattern is the one to have in the toolbox. It is a few more lines than ALTER TABLE ADD COLUMN and it is the difference between a five-minute deploy and an outage.

IcarusRegular#3

In reply to Devn Nie (#2)

The "idle in transaction" pattern is the one that has bitten us most, and it is not the migration.

A request handler that opens a transaction, calls something slow outside the database, and commits at the end holds locks for the whole call. A migration that needs ACCESS EXCLUSIVE then waits behind it, and the queue behind the migration is invisible in the logs because the application just looks slow.

EricAuthorMember#4

In reply to Icarus (#3)

It was exactly that, and the answer to "what do I do now" turned out to be "find the transaction that is not committing".

The migration was queued behind a reporting job that opened a transaction, called an API, and took forty seconds to come back. The migration finished on its own once that cleared, and the lock timeout is going in the same deploy so the next one fails fast instead of waiting.