Skip to content

fix(coderd/database/migrations): stop upgrades from deadlocking with chat traffic - #30307

Open
ThomasK33 wants to merge 6 commits into
mainfrom
tk/codagt-1298-migration-deadlock-retry
Open

ThomasK33 wants to merge 6 commits into
mainfrom
tk/codagt-1298-migration-deadlock-retry

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Migration upgrades no longer deadlock with chat traffic from replicas that keep serving during a rolling deploy. coder server runs all pending migrations in one transaction, and older chat migrations (000600, 000601, 000607, 000608) take and upgrade locks on chats, chat_messages and chats_expanded in orders that cycle with chatd. #30297 fixed 000609 and 000611 only. Upgrades from 585 (v2.37.3), 600, 605 (release/2.38) and 607 still deadlocked: from 585 PostgreSQL killed the migrator, from the others it killed user requests.

The fix lives in the migrator, so it covers every entry point (coder server with external or built-in Postgres, dbtestutil, gen/dump, scripts/migrate-ci, scripts/migrate-test), and future chat migrations get it for free:

  1. Lock first. Before the first pending migration runs, the migration transaction locks chats and every table with a foreign key path to it (chat_messages, chat_queued_messages, chat_automations and the other chat child tables, found from pg_constraint) in ACCESS EXCLUSIVE mode, and chats_expanded with a no-op ALTER VIEW (it locks only the view; LOCK TABLE on the view would also lock users). It uses the 000609 pattern: short waits (lock_timeout = min(100ms, deadlock_timeout/2)), release and retry, 2-minute deadline. Because the migrator gives up before deadlock_timeout, PostgreSQL does not pick a chat request as the victim. Attempts alternate between locking the view before and after the tables: with a fixed order, sustained traffic of the opposite order made every attempt time out (measured locally: never migrated in 20 s; alternating: under 0.3 s for each kind of load).
  2. Retry on deadlock. If the run still fails with SQLSTATE 40P01, UpWithFS reruns it from the committed version (up to 5 attempts, 100ms to 800ms backoff, one warning per retry). Other errors are never retried.
Design choices and residual risk
  • The lock block runs only when at least one migration is pending (golang-migrate calls Run only for pending migrations). A normal restart with nothing pending takes no chat lock. When anything is pending, chat traffic waits until the migration transaction commits, even if no pending migration touches chat tables. That is the accepted cost: almost every release alters chats, and such a migration holds ACCESS EXCLUSIVE on it until commit anyway. Scanning migration SQL for chat references could miss an indirect reference.
  • On a fresh database chats does not exist and the block does nothing.
  • Only the chat relations are in the block. Later migrations still wait for other tables without a timeout while the chat locks are held. An application transaction that holds a lock on such a table for longer than deadlock_timeout and then touches a chat table can be aborted (remote UAT reproduced this with connection_logs and 000603 from 600; on main the same sequence does not deadlock, because chats is not locked yet at 000603). If it touches the chat table sooner, the migrator is aborted and the retry recovers. Locking more tables up front, such as users for 000609's foreign keys, would block all writes to them for the whole run.
  • Known limitation, shared with the 000609 block: a saturating mix of both chat lock orders can keep every short-wait attempt failing until the 2-minute deadline, and the server then exits without migrating (the database is unchanged; a restart retries).
  • golang-migrate's database.Error has no Unwrap, so the deadlock check walks OrigErr and joined errors by hand. A failed run leaves the golang-migrate instance locked, so each attempt builds a new one. Lock now rolls back when taking the advisory lock fails, so a retry never leaves an open transaction behind.

Tests

  • TestUpWithFSChatTrafficDuringUpgrade upgrades databases at 585, 600, 605, 607 and 608 to latest with the real UpWithFS, under the four traffic patterns from migration000609_test.go (lock chat then update, insert message then update chat, read chats_expanded while the migration waits, lock chat then read chats_expanded). It sequences with pg_blocking_pids, not sleeps, and requires no application 40P01, no migrator retry, and a clean final version. With the lock block disabled, 585, 600, 605 and 607 fail with deadlocks on one side or the other.
  • TestUpWithFSChatAutomationTraffic upgrades from 610 while an application transaction updates a chat automation's target_chat_id and a later migration locks chat_automations. Before the lock set included tables that reference chats, the update's foreign key check deadlocked with that migration.
  • TestUpWithFSRetriesDeadlocks covers one retry, no retry for other SQLSTATEs, and giving up after 5 attempts.
  • TestUpWithFSChatTableLocks covers that nothing-pending runs take no chat lock and that the view lock leaves chats_expanded unchanged.
  • Dogfood UAT ran remotely through Coder Agents against the pushed branch (two rounds; the second, on the alternating lock order, passed: upgrades under chat traffic and pgbench load, fresh and already-migrated databases, and the lock deadline).

Closes CODAGT-1298
Part of CODAGT-1135


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

…tables first

Signed-off-by: Thomas Kosiewski <tk@coder.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…able locks

The chat table lock step lowers lock_timeout while it retries its locks and
must restore it. No test failed when the restore was removed: later
migrations would then give up on any lock wait longer than 100ms. The no-op
migration in TestUpWithFSChatTableLocks now fails if lock_timeout differs
from its session value.

_Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high_

Signed-off-by: Thomas Kosiewski <tk@coder.com>
…orten the lock error

Sustained chat traffic of one kind starved a fixed lock order: transactions
that lock a chat row and then read chats_expanded defeat view-first
attempts, and readers of the view defeat chats-first attempts. Attempts now
alternate the order. The lock step's error no longer repeats the whole DO
block, and the comment states the residual risk for later migrations.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
@linear-code

linear-code Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

CODAGT-1135

CODAGT-1298

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T00:03:00.950985Z ef4b15d Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: aec2c85ae2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ts up front

An application write to chat_automations locks that table and then waits
for chats in its foreign key check. The migration transaction holds chats
from its start, so a later migration that locks chat_automations deadlocked
with that write (found by the CODAGT-1223 lane on its 000612). The lock step
now locks chats and every table with a foreign key path to it, found from
pg_constraint, and a test covers an automation update during an upgrade
from 610.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a22894dca4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/database/migrations/txnmigrator.go Outdated
lock_timeout applies to each lock acquisition, so an attempt that waited
almost 100ms for each of several tables could hold chats for longer than
deadlock_timeout. Each attempt now shares one budget of half of
deadlock_timeout, and every lock wait is capped at the time left.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: ef4b15d17f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

This branch has not been deployed

No deployments
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.

1 participant