Two pilot follow-ons: rustc drift warning + blob GC

Both surfaced by the 2026-07-12 pilot as real operator concerns:

## rustc drift warning at build time

Runners silently silo their cache when rustc versions differ across
peers (fingerprint depends on rustc verbose output). The pilot's
first flow burned a full cold+upload before we realized the silo.

- `PeerStatusReply.local_rustc_release` — new field, populated from
  the peer's own gossip `RUSTC_RELEASE` key via a new
  `ClusterGossip::self_kv(key)` accessor.
- `claw-cargo build`: on cache MISS, calls `PeerStatus`; if the
  peer's rustc release ≠ our local `rustc --version`, emits a WARN
  with both versions + hint to add `rust-toolchain.toml`.
- Best-effort: absence of either release string is a shrug, not
  an error.

## blob GC

Blob store grows unbounded on a runner; disk-full is a real
incident. `gc_orphan_chunks` already existed but wasn't exposed.

- New CLI: `claw-store cluster-gc` — runs `gc_orphan_chunks`,
  prints report. Safe to run any time, safe to interrupt.
- New config: `cluster.gc_interval_hours: Option<u64>`. When set to
  a positive integer, the daemon spawns a periodic ticker that
  invokes GC in-process. Skips the first tick (nothing to reclaim
  on boot). Errors are logged and retried next tick.
- Shutdown aborts the ticker cleanly.

254 tests pass (baseline unchanged). Pre-existing macOS failure
untouched.
This commit is contained in:
Omar Sobh
2026-07-12 06:23:46 -07:00
parent 54e9da4d62
commit 84aa758fd4
6 changed files with 177 additions and 2 deletions
+9
View File
@@ -154,6 +154,13 @@ pub struct ClusterConfig {
/// group). Absent means "no scrape endpoint".
#[serde(default)]
pub prom_bind: Option<SocketAddr>,
/// Field finding 2026-07-12: how often the daemon runs
/// `gc_orphan_chunks` to reclaim disk from chunks no live
/// manifest references. `None` or `0` disables auto-GC — the
/// operator can still invoke `claw-store cluster-gc` by hand.
/// Typical value: `6` hours on a runner cache.
#[serde(default)]
pub gc_interval_hours: Option<u64>,
}
/// Compute the default RPC address for a gossip address: same IP, port + 1.
@@ -412,6 +419,7 @@ tailscale_addr = "100.64.1.5:7701"
tls: None,
blob_store_root: None,
prom_bind: None,
gc_interval_hours: None,
};
let err = cluster.validate().unwrap_err().to_string();
assert!(err.contains("no bind address"), "unexpected error: {err}");
@@ -442,6 +450,7 @@ tailscale_addr = "100.64.1.5:7701"
tls: None,
blob_store_root: None,
prom_bind: None,
gc_interval_hours: None,
};
let err = cluster.validate().unwrap_err().to_string();
assert!(