The two biggest pain points coming out of the architecture review:
1. The manifest at /var/lib/claw-store/projects.toml was the only
piece of writable state but had no locking, no atomic writes,
and three concurrent writers (daemon poll tick, every CLI verb,
and the dashboard shelling out via /api/activate). Two writers
interleaving silently dropped one of them; a crash mid-write
left a corrupt half-written TOML that the next reader parsed
as an empty manifest.
2. Reboot survival: the dashboard had no systemd unit and was a
stray hand-launched process. Architect lost its dashboard on
todays reboot.
This commit lands:
- Manifest::update(path, FnOnce(&mut Manifest)) — locked-atomic
load-mutate-save in one transaction. Uses libc::flock(LOCK_EX) on
a sidecar .lock file (so the data file can be replaced by rename
without invalidating the lock) and tempfile + persist for the
rename. Concurrent writers serialise; readers see the previous
state or the new state, never a torn write. Manifest::load uses
LOCK_SH so it never races a mid-rename.
- Project.pinned: bool with #[serde(default)] so legacy manifests
parse cleanly. hot::gc_stale_targets and hot::gc_by_space both
skip pinned projects, with a WARN log when every remaining
project is pinned but were still over budget — operator intent
beats space pressure.
- claw-store pin <project> / unpin <project> CLI verbs.
status command surfaces pin marker (📌).
activate preserves an existing rows pinned flag so re-activating
doesnt silently unpin.
- claw-store-serve.service systemd unit. Type=simple, Restart=
on-failure, RestartSec=15, ProtectSystem=strict + ReadWritePaths
=/var/lib/claw-store, ProtectHome, NoNewPrivileges, PrivateTmp.
- daemon poll tick reloads the manifest from disk at the start of
each cycle (so CLI activations between ticks are visible) and
routes its GC write through Manifest::update (so it cant race a
concurrent CLI pin).
- libc + tempfile move from dev-deps into runtime deps.
- empty-manifest fallthrough on load (treat "" as default) so a
half-written tempfile crashed pre-rename doesnt hard-fail the
daemon next boot.
- 25 tests passing incl. new ones: legacy-toml-parses, update-
serializes-two-sequential-writers, pinned-survives-stale-gc,
pinned-survives-space-gc-even-when-lru.
Version bumped 0.1.0 → 0.2.0.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
34 lines
987 B
TOML
34 lines
987 B
TOML
[package]
|
|
name = "claw-store"
|
|
version = "0.2.0"
|
|
edition = "2021"
|
|
|
|
[[bin]]
|
|
name = "claw-store"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
clap = { version = "4", features = ["derive"] }
|
|
serde = { version = "1", features = ["derive"] }
|
|
toml = "0.8"
|
|
tokio = { version = "1", features = ["full"] }
|
|
sysinfo = "0.30"
|
|
anyhow = "1"
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
chrono = { version = "0.4", features = ["serde"] }
|
|
axum = { version = "0.7", features = ["macros"] }
|
|
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
|
tokio-stream = "0.1"
|
|
serde_json = "1"
|
|
# v0.2.0 — flock(2) wrapper for atomic+locked manifest writes
|
|
# (manifest.rs). Already a transitive dep; declaring it directly
|
|
# makes the call site obvious.
|
|
libc = "0.2"
|
|
# v0.2.0 — sibling-tempfile + rename for atomic manifest persistence.
|
|
# Was previously dev-dep only; promoted to main.
|
|
tempfile = "3"
|
|
|
|
[dev-dependencies]
|
|
tempfile = "3"
|