|
| 1 | += Architecture Decision Record: 0013-json-sidecar-cross-process-locking |
| 2 | +<!-- SPDX-License-Identifier: PMPL-1.0-or-later --> |
| 3 | +<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> --> |
| 4 | + |
| 5 | +# 13. Cross-process locking + atomic writes for the JSON sidecar |
| 6 | + |
| 7 | +Date: 2026-05-31 |
| 8 | + |
| 9 | +## Status |
| 10 | + |
| 11 | +Accepted (2026-05-31) — implemented in #151. Closes #150 (V-L2-F4). |
| 12 | +**Refines [ADR-0012](0012-json-family-sidecar-storage.adoc)** (V-L2-F3): it |
| 13 | +does not reverse the JSON-family decision, it hardens its concurrency story. |
| 14 | + |
| 15 | +## Context |
| 16 | + |
| 17 | +ADR-0012 implemented the JSON-family sidecar with crash-safe writes |
| 18 | +(temp file + same-directory atomic `rename`) but a **single-writer-by- |
| 19 | +assumption** concurrency model: it noted that the SQLite path serialises |
| 20 | +writers via the database write lock, whereas the JSON path assumed one |
| 21 | +writer at a time. Two processes that each ran the load→mutate→save cycle |
| 22 | +against the same sidecar could interleave and silently lose writes — a real |
| 23 | +hazard for `gc` running alongside an appending process. |
| 24 | + |
| 25 | +The `provenance` and `history` CLI subcommands were also still stubs, so the |
| 26 | +JSON (and SQLite) sidecars could be written but not yet *queried* from the |
| 27 | +CLI. |
| 28 | + |
| 29 | +## Decision |
| 30 | + |
| 31 | +Harden the JSON store and wire the read-side CLI (#151): |
| 32 | + |
| 33 | +* **Cross-process advisory write lock.** A dependency-free lock file |
| 34 | + (`<path>.lock`) is created exclusively, with bounded backoff and a |
| 35 | + stale-lock *steal* heuristic, and released via RAII on drop. It is held |
| 36 | + across the **entire** load→mutate→save cycle, so a concurrent `gc` and |
| 37 | + `append` serialise instead of clobbering each other. (`src/sidecar/lock.rs`.) |
| 38 | +* **Atomic durability, formalised.** The temp-file + same-directory `rename` |
| 39 | + in `save()` is documented as the durability boundary and is now performed |
| 40 | + under the write lock. |
| 41 | +* **`provenance` / `history` wired.** `provenance <entity>` lists the chain |
| 42 | + and reports `verify_chain` plus any fork points; `history <entity> |
| 43 | + [--at T]` lists temporal versions (or the point-in-time snapshot at `T`). |
| 44 | + Both work against the SQLite *and* JSON backends; `postgres` reads remain |
| 45 | + an explicit "not yet implemented". |
| 46 | +* **Release.** Version `0.1.0` → `0.2.0`, with a CHANGELOG entry. |
| 47 | +
|
| 48 | +## Consequences |
| 49 | + |
| 50 | +### Positive |
| 51 | + |
| 52 | +- Concurrent cross-process writers to a JSON sidecar are safe: serialised by |
| 53 | + the advisory lock and crash-safe via atomic rename. This closes the main |
| 54 | + parity gap ADR-0012 called out against the SQLite store. |
| 55 | +- No new dependencies — the lock is built on the standard library. |
| 56 | +- `provenance` and `history` now function against real sidecars, for both |
| 57 | + storage families. |
| 58 | +
|
| 59 | +### Negative |
| 60 | + |
| 61 | +- The lock is **advisory** (cooperative) with a stale-steal heuristic: a |
| 62 | + writer wedged past the staleness window can have its lock stolen. This is |
| 63 | + the documented trade-off of a dependency-free, portable lock. |
| 64 | +- Writes remain whole-file `O(n)` rewrites (no incremental append for the |
| 65 | + JSON encodings) — acceptable for the small-to-medium sidecars this store |
| 66 | + targets, but not for very large histories. |
| 67 | +- `postgres` provenance/history reads are still unimplemented (tracked |
| 68 | + separately). |
| 69 | +
|
| 70 | +## References |
| 71 | + |
| 72 | +- Issue #150 (V-L2-F4); PR #151. |
| 73 | +- Refines ADR-0012 (V-L2-F3, the JSON-family backend; issue #146 / #148). |
| 74 | +- ADR-0010 (provenance forks are first-class) — the chain semantics the |
| 75 | + JSON store mirrors. |
0 commit comments