fix(docs): correct fabricated API/behavior claims in the locks docs

docs/API.md's Locks section described a timeoutMs request param, a
408-timeout response, and field names (acquiredAt/acquiredMs) that
don't exist anywhere in the actual routes.js — the server is single-shot
and never blocks; timing out is a CLI-only concept. Also fixes the
section being spliced into the middle of the pre-existing Sessions
heading and its content.

docs/LANES.md said the owner file stores an epoch in milliseconds
(it's seconds), that the CLI polls every ~1s (it's ~2s), that the lane
card shows a "waiting" state (no such server-side concept exists, only
who currently holds), and included a fabricated "manually transfer a
lock's holder identity" procedure that also contradicted the
never-touch-the-lock-directory etiquette rule stated right above it.
This commit is contained in:
2026-08-04 14:02:40 +07:00
parent 6c7554dbdc
commit 465dca35e5
2 changed files with 33 additions and 66 deletions
+26 -59
View File
@@ -397,95 +397,62 @@ resolved path is confined to the lane's log directory after `realpath`, so a
name from the request can never escape it; anything else is `404 ENOLOG`. A lane
with no slot returns `{"available": false}`.
### Sessions
### Locks
Cross-lane named locks (`server/lib/named-lock.js`) — the OTHER axis from a
lane's own internal serialization: a name held by one lane at a time, across
ALL lanes, for a step that thrashes the shared machine (a build, an e2e run).
Every route here is **single-shot** — it never blocks waiting for a lock to
free up. A caller that wants to wait polls `POST /:name/acquire` itself (the
`ccam lock acquire` CLI does exactly this, printing a status line every ~60s
of continued waiting); the server never orchestrates that loop.
#### List locks
```http
GET /api/locks
```
Returns all currently-held named locks:
Every currently-held lock:
```json
{
"locks": [
{
"name": "build",
"holder": "lane3",
"acquiredAt": 1722702012345,
"acquiredMs": 1722702012345
}
]
"locks": [{ "name": "build", "holder": "lane3", "since": 1722702012, "ageSec": 41 }]
}
```
Each lock has a `name`, the current `holder` (defaults to `lane<slot>`), and `acquiredAt` / `acquiredMs` (the millisecond timestamp when acquired). If no locks are held, the array is empty.
`since` is a Unix **seconds** timestamp; `ageSec` is derived at read time. An
empty array means nothing is held.
#### Acquire a lock
```http
POST /api/locks/:name/acquire
{ "holder": "lane3" }
```
Request body:
`holder` is **required** — this route does not default it; `ccam lock
acquire` resolves `lane<slot>` for the calling lane and sends it explicitly.
A holder older than `LOCK_MAX_HOLD` (default 2700s, floored at 300s — see
`docs/LANES.md#cross-lane-named-locks`) is broken automatically before the
attempt.
```json
{
"holder": "lane3",
"timeoutMs": 30000
}
```
`holder` defaults to the calling lane (`lane<slot>`) if omitted. `timeoutMs` is optional; without it, the request waits indefinitely. Returns **200** with the acquired lock object:
```json
{
"name": "build",
"holder": "lane3",
"acquiredAt": 1722702012345
}
```
or **408** if the timeout elapses before the lock becomes free:
```json
{
"error": "ETIMEOUT",
"waited": 30000,
"currentHolder": "lane1",
"message": "Lock held by lane1, timeout elapsed"
}
```
- **200** `{ "acquired": true }` — lock claimed.
- **409** `{ "acquired": false, "holder": "lane1", "since": 1722701900, "ageSec": 150 }` — already held by someone else (fresh enough not to be broken).
- **400** `{ "error": { "code": "EBADHOLDER", "message": "holder is required" } }` — missing/empty `holder`.
#### Release a lock
```http
POST /api/locks/:name/release
{ "holder": "lane3" }
```
Request body:
- **200** `{ "ok": true }` — released.
- **409** `{ "error": { "code": "ENOTHOLDER", "message": "...", "currentHolder": "lane1" } }` — `holder` doesn't match the current owner; a release never trusts its caller, same rule every other destructive path in this project follows.
- **404** `{ "error": { "code": "ENOLOCK", "message": "no such lock: build" } }` — nothing by that name is held.
```json
{
"holder": "lane3"
}
```
`holder` defaults to `lane<slot>` if omitted. Returns **200** with the released lock object, or **409** if the holder does not match:
```json
{
"error": "ENOTHOLDER",
"name": "build",
"expectedHolder": "lane3",
"actualHolder": "lane1",
"message": "Lock is held by lane1, not lane3"
}
```
### Sessions
#### List Sessions