| 1 | # 0007 — Optimistic concurrency, never locks |
| 2 | |
| 3 | **Status:** adopted |
| 4 | |
| 5 | ## Decision |
| 6 | |
| 7 | No lock service, no lease, no counter service. Concurrent pushes are resolved |
| 8 | entirely by the conditional `PUT` in [0005] |
| 9 | bounded retry loop with jittered backoff. |
| 10 | |
| 11 | ## Where ` |
| 12 | |
| 13 | `seq` is `index.seq + 1`, allocated by the writer. It cannot collide because |
| 14 | **allocating it and publishing it are the same atomic act**. Two pushes both |
| 15 | read `seq = 5` and both want 6; one wins the CAS, the other's CAS fails, it |
| 16 | re-reads (now sees 6) and retries as 7. |
| 17 | |
| 18 | ## Why validation lives inside the loop |
| 19 | |
| 20 | Losing the CAS is not the same as failing the push. On each attempt the loop |
| 21 | re-reads the index and re-checks every transition's `oldOid` against the live |
| 22 | ref: |
| 23 | |
| 24 | - Push to a different branch → still valid → retries and lands at the next seq. |
| 25 | - Push to the same branch someone just moved → `oldOid` no longer matches → |
| 26 | `NonFastForwardError`, which is **not |
| 27 | |
| 28 | Validating once outside the loop would be a lost-update bug that only appears |
| 29 | under concurrency. |
| 30 | |
| 31 | ## Git supplies the precondition |
| 32 | |
| 33 | This is not an invented protocol. `receive-pack` sends `<old-oid> <new-oid> |
| 34 | <ref>` per command. That `old-oid` *is |
| 35 | own compare-and-swap from a local file lock to object storage; it does not add |
| 36 | new semantics. |
| 37 | |
| 38 | ## The ambiguity case |
| 39 | |
| 40 | A `PUT` that times out may still have landed. Retrying blindly would find the |
| 41 | `oldOid` no longer matching and report non-fast-forward for a push that |
| 42 | succeeded. The loop therefore checks for its own ULID in `index.layers` first |
| 43 | (see [0006](0006-entries-carry-no-seq.md |
| 44 | |
| 45 | ## Consequence |
| 46 | |
| 47 | `MAX_ATTEMPTS` is 8, after which `WalContentionError` returns 503. Pushes to one |
| 48 | repository serialize even across disjoint refs. That is acceptable: pushes are |
| 49 | rare and short. Per-ref logs would remove the contention and destroy the total |
| 50 | order that makes replay deterministic — not a trade worth making. |