0029 — Integrates are serialized by a Postgres advisory lock, and contention is a Defer¶
Status: accepted · Date: 2026-08-19 (recorded 2026-09-15) · Extends: ADR-0002, ADR-0008, ADR-0009
Context¶
Phase ② builds work items in parallel across worktrees (ADR-0008) and merges each into one integration branch per cycle. The integration branch is a single shared git ref: two workers merging into it at once corrupt each other, and nothing serialized them — the integrate handler's own docstring said so. Once rotation and multiple workers went live (#41, #42) the race was real.
ADR-0002 anticipated advisory locks for phase transitions (pg_advisory_xact_lock in the data-model plan). What actually needed serializing was the integrate, and an integrate is not a transaction: it is a sequence of git subprocesses and ledger writes over many seconds.
Decision¶
build.integrate for one (project_id, cycle) runs under a session-level Postgres advisory lock, acquired with pg_try_advisory_lock, and a failed acquisition is a short Defer.
IntegrationLockis an application seam (try_acquire/release), implemented byPostgresAdvisoryLock. The key is the first 8 bytes ofsha256("vibey.integrate:<project>:<cycle>")as a signed 64-bit integer, namespaced so it can never collide with any other advisory-lock use in the database.- Session-level, deliberately. A session lock lives on the connection that took it, so the held lock pins its pooled connection out of the pool until release. Releasing the connection back would silently drop the lock the moment another query reused the session. A transaction-level lock cannot span a merge that runs outside any transaction.
- Never block.
try_acquirereturnsFalseon contention and the job defers; a worker never waits on a lock. The never-block-a-worker rule (ADR-0009) applies to locks as it applies to humans. - Release in a
finally, after the kind/work-item guards, so a crashed merge cannot wedge the branch for every other worker; a lock whose holder dies is released by Postgres when the connection drops.
Consequences¶
Good. N workers share one integration branch safely with no coordinator, using the database already required by ADR-0002. Cross-connection contention is proven against real Postgres in the suite, and all faked end-to-end runs carry the real lock in the path.
Bad. One pooled connection is unavailable for the duration of a merge; pool size must allow for it. A deferred integrate looks idle to an observer for one backoff.
Rule status. The mechanism does not pass ADR-0020's test. The clause "a worker never blocks on a lock; contention is a Defer" extends the standing never-block rule, and belongs in the sub-doctrine ADR-0009 owes rather than one of its own.
Alternatives rejected¶
pg_advisory_xact_lock(the plan). Scoped to a transaction; the merge is not one.- A git-side lock (
flockon the repository,git update-refCAS). Local to one filesystem; fails across pods sharing a database but not a disk. - A row lock on the cycle record. Held for the merge's duration it blocks every other reader of that row, and it blocks rather than defers.
- A single dedicated integrate worker. Removes parallel integrates entirely and reintroduces a coordinator the queue was designed not to need.
- Blocking
pg_advisory_lock. Simpler, and exactly the thing the never-block rule forbids.