docs(locks): document cross-lane named locks (D)

This commit is contained in:
2026-08-04 11:41:54 +07:00
parent c068f0cac6
commit 6c7554dbdc
5 changed files with 135 additions and 3 deletions
+40
View File
@@ -830,6 +830,46 @@ curl -X POST http://localhost:4820/api/lanes/5/remove \
**Actions gated behind confirmation:** `remove` requires the `confirm` flag to prevent accidental deletion.
## Cross-lane named locks
Cross-lane named locks serialize work that thrashes a shared machine — builds, e2e runs, database migrations — across all lanes, not just within one lane. This is a separate axis from `withLaneLock`, the in-process per-lane lock in `server/lib/lane-lock.js`: `lane-lock` is about _when_ a lane runs internal operations; `named-lock` is about which _other lanes_ must wait.
### How they work
- **Atomicity via `mkdir`.** Lock ownership is declared via an `owner` marker file in the lock directory. `mkdir` atomically creates or fails with `EEXIST` — no race between check and create.
- **Holder file format:** `<lock-name>` is owned by a holder string (defaults to `lane<slot>` for the calling lane) that writes `LANES_ROOT/.locks/<lock-name>/owner`. The format is `<holder> <acquired-epoch-ms>` — the holder identity and a timestamp. When a new acquire finds the directory already exists but the holder is stale (more than `LOCK_MAX_HOLD` seconds old, default 2700s / 45 minutes), the old holder is considered dead and the directory is removed before acquiring.
- **The `LOCK_MAX_HOLD` floor.** The default is 2700 seconds, but it has a **hard floor of 300 seconds**. No caller can force-break a live holder by setting `LOCK_MAX_HOLD=1` — the floor prevents that mistake. The lowest possible timeout is 300 seconds, even if `LOCK_MAX_HOLD` is overridden to something smaller.
- **Waiting and polling.** `ccam lock acquire` blocks the calling lane (polling the filesystem every ~1 second) until the lock is free or the `--timeout` expires. While waiting, the caller heartbeats its own presence at ~60-second intervals, so a waiting lane never reads as stalled. This means the CLI owns the polling loop — the server is stateless and does not queue or defer — which keeps CCAM's primitives non-orchestrating.
### The CLI
```bash
ccam lock status [<name>]
```
Show one named lock's current holder, or every currently-held lock. `<name>` is optional — without it, lists all locks.
```bash
ccam lock acquire <name> [--holder X] [--timeout N]
```
Acquire a cross-lane named lock, polling until it is free or the timeout expires. `--holder` defaults to `lane<slot>` (the calling lane's identifier based on its slot). `--timeout` is in seconds; without it, the command waits indefinitely.
```bash
ccam lock release <name> [--holder X]
```
Release a lock. Refused with status `409` if the `--holder` does not match the current owner. The `--holder` default is the same as `acquire`: `lane<slot>`.
### The lane card
The lane card displays a lock badge when the lane is waiting for or holding a named lock. The badge is polled every 30 seconds (the same interval as git facts and runtime facts), so you see the lock status without a page refresh.
### Etiquette
**Waiting is normal.** A lane that sits at a lock for a few minutes while another finishes a build is expected behavior, not a failure. Do not interrupt or force-break a lock.
- **Never kill a holder.** If a lane is stuck holding a lock, do not kill the process or the dashboard. Investigate why the holder is not releasing it.
- **Never delete the lock directory by hand.** If a lock persists after the processes that held it are gone (e.g., after a hard reboot), let `LOCK_MAX_HOLD` and the staleness detection clean it up naturally. If the wait cannot survive that long, the holder string can be changed in a new `acquire` call — a lane that was `lane3` can be manually transferred to `lane5` by a human operator reading the owner file and calling `acquire --holder lane5`, but this is last-resort only.
- **Never shrink `LOCK_MAX_HOLD` to force through a wait.** The 300-second floor exists specifically to stop this mistake. If the true holder is gone, the 300-second minimum wait is the price of safety. If the true holder is still running (e.g., a build with a network stall), shortening the timeout from 2700 to 300 does not help — it converts a slow success into a premature timeout, leaving the lock held and every other lane blocked forever.
## Orchestration: what CCAM does NOT do
**CCAM does not chain, queue, retry, or evaluate gates.**