Three-Phase Commit (3PC) protocol — concept page. Adds PreCommit phase between Vote and DoCommit to fix 2PC blocking problem when coordinator fails. Three scenarios: (1) happy path through CanCommit/PreCommit/DoCommit, (2) coordinator failure after PreCommit with non-blocking termination via participant election, (3) network partition causing split-brain (3PC only correct in synchronous crash-stop model). Includes ADRs explaining why 3PC is rarely used in production (Spanner Paxos commit, Saga, Outbox patterns are preferred).
Mental model: 3PC = 2PC + промежуточная фаза PreCommit, которая ломает блокирующее ожидание участников при падении координатора. Если все участники получили PreCommit — каждый знает, что все остальные тоже проголосовали YES, и любой выживший может безопасно завершить транзакцию commit-ом без координатора. Цена: лишний round-trip. Условие: synchronous network, no partitions. В реальной асинхронной сети 3PC ломается ровно так же, как любой алгоритм без consensus.
2PC имеет одну неизлечимую болезнь — blocking problem. Участник проголосовал YES, записал prepared-запись в WAL, удержал locks — и теперь обязан ждать решения координатора. Если координатор упал между Phase 1 (vote) и Phase 2 (decision), участник застрял: ни commit, ни abort самостоятельно сделать нельзя. Locks держатся, очереди читателей растут, downstream сервисы тайм-аутят, метрики краснеют. Восстановление = ручной DBA с pg_xact_status или ждать возврата координатора (single point of blocking).
3PC проектировался как академический ответ на эту проблему. Skeen (1982) предложил вставить между vote и decision дополнительную фазу PreCommit. Ключевой инвариант: если участник перешёл в PRE-COMMIT state, значит все участники проголосовали YES, и все участники тоже либо уже в PRE-COMMIT, либо туда дойдут. Это значит: при падении координатора выжившие участники могут договориться между собой и завершить транзакцию без блокирования.
Звучит красиво. Не используется почти нигде. Почему — см. ADR-002 и сценарий partition-breaks-3pc.
Phase 1: CanCommit? -> голосование (как в 2PC)
Phase 2: PreCommit -> "все voted YES, готовьтесь" (новая фаза)
Phase 3: DoCommit -> "commit прямо сейчас" (как Phase 2 в 2PC)
Каждая фаза = round-trip coord <-> participants. 2PC = 2 round-trip-а, 3PC = 3. На LAN с 1ms ping-ом разница смешная (3ms vs 2ms), на cross-region с 80ms ping-ом — 240ms vs 160ms на каждую транзакцию.
Инвариант, ради которого всё затеяно: между концом Phase 2 и началом Phase 3 любой участник знает, что транзакция точно будет committed. Поэтому если в этом промежутке координатор умер, выживший участник может: (a) дождаться timeout, (b) elect new coordinator, (c) опросить других, (d) если кто-то в PRE-COMMIT — все коммитят, (e) если никто в PRE-COMMIT — все abort-ят.
Один координатор (TM, transaction manager) и три участника-БД: orders, inventory, payments — классический distributed transaction поверх трёх шардов. Client инициирует BEGIN tx, координатор оркестрирует три фазы. Три сценария показывают: happy path с тремя round-trip-ами, координатор падает после PreCommit (3PC работает — выигрыш!), сеть partition-ится во время Phase 2 (3PC ломается — divergent state).
Connect-ы в топологии (topology.connect) — это физические соединения. Все сообщения 3PC (CanCommit / PreCommit / DoCommit / ACK) ходят по одним и тем же tcp-edge-ам в обе стороны — анимация использует reverse-flow на тех же edges (см. CLAUDE.md, секция "Edges vs Animation").
1. happy-path-3-phases — три полных round-trip-а. Phase 1: CanCommit? → 3 YES. Phase 2: PreCommit → 3 WAL-записи → 3 ACK. Phase 3: DoCommit → 3 apply → 3 ACK → 200 OK клиенту. Видно цену: на LAN это ~150ms total, на cross-region уже неприемлемо. Сравните с одиночным INSERT на одном узле (~5ms) — distributed transaction любым алгоритмом стоит дорого.
2. coordinator-failure-after-precommit — момент славы 3PC. Все участники получили PreCommit, записали в WAL, ACK-нули. Coordinator падает перед рассылкой DoCommit. У всех участников истёк timer. Они elect нового координатора (роль берёт p1), обмениваются state-ом: «я в PRE-COMMIT» / «я в PRE-COMMIT» / «я в PRE-COMMIT». По инварианту: раз все в PRE-COMMIT, значит координатор дошёл до Phase 2, значит все проголосовали YES, значит commit безопасен. Новый координатор рассылает DoCommit, транзакция завершается. В 2PC участники здесь повисли бы навсегда.
3. partition-breaks-3pc — реальный мир ломает академическую модель. Координатор успел отправить PreCommit только p1. Сеть split-ится: {coord, p1} в одной partition, {p2, p3} в другой. На side A: координатор шлёт DoCommit p1, p1 коммитит. На side B: p2/p3 не получили PreCommit, у них timeout, они рассуждают: «я НЕ в PRE-COMMIT state → значит координатор умер до Phase 2 → safe to abort». p2 и p3 abort-ят. Сеть восстанавливается — orders имеют новую запись, inventory не списан, payments не списан. Broken invariant.
Это и есть демонстрация того, почему 3PC не используют: его корректность опирается на reliable failure detection («если timeout, точно crash»), которая в асинхронной сети недостижима. partition неотличим от crash с точки зрения участника, и 3PC принимает неправильное решение.
ADR-001: Зачем нужна третья фаза — non-blocking при coordinator failure. Решает 2PC blocking problem ценой +50% latency и +50% сетевого трафика. На каждой транзакции лишний round-trip. На p99-чувствительных API (платежи, чекаут) эта надбавка часто несущественна по сравнению с потенциальным простоем в minutes/hours при крахе координатора 2PC. Но условие применимости — узкое.
ADR-002: Почему 3PC всё равно не используется в production. Skeen доказал корректность только в синхронной модели с надёжной failure detection и без network partitions. Реальные сети асинхронны: пакет может задержаться на минуты, partition неотличим от crash, timeout — ненадёжный сигнал. Отрасль пошла другим путём:
3PC остался теоретическим артефактом для университетских курсов: его изучают, чтобы понять trade-offs и почему «добавим ещё одну фазу» — не путь к надёжности в реальных сетях.
Практически — никогда не использовать в production. 3PC ценен как учебный материал: понять, почему «добавить фазу» не спасает, и почему индустрия в итоге выбрала Paxos/Raft.
Конкретно НЕ применять:
Если очень хочется non-blocking distributed commit — берите Spanner-style Paxos commit или Calvin-style deterministic ordering, не 3PC.
::concept{slug="two-phase-commit"}, ::concept{slug="cap-theorem"}, ::concept{slug="saga-pattern"}.