Phase 4b follow-on: pin --ttl RPC + CLI
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 10s

Closes Phase 4b by exposing the TTL sidecar written by
Phase 4b primitives on the wire and via `claw-cargo pin`.

Wire additions:
* Method::SetTagExpiry (0x1a) — payload `key_len:u16 || key ||
  expires_at:u64 (LE)`. Reply single-byte OK. `expires_at == 0`
  clears the sidecar.
* Method::GetTagExpiry (0x1b) — payload raw key bytes. Reply 8
  bytes (u64 LE) on hit; NotFound when no sidecar is present.

Both accept writes even when the stamped tag itself is absent,
matching `TagStore::set_stamped_expiry` semantics — the sidecar
takes effect the moment the tag lands.

CLI:
* `claw-cargo pin --ttl <duration>` — humantime-style duration
  (`30d`, `1h30m`, `2w`, ...). Applied to both the primary tag
  and its `.fingerprint` companion so eviction treats them as
  one lifetime. `--ttl 0` / `clear` / `none` clears an existing
  sidecar without touching the value.

Tests: encode/decode roundtrip + malformed-input rejection for
`encode_expiry_record`, method-byte stability, NotConfigured
without a tag store, end-to-end set/get/overwrite/clear over
QUIC, and a real-pin flow that publishes a stamped tag then
attaches TTL. Duration parser is unit-tested for single/compound
forms, case-insensitive units, bad input, and clock alignment.

No new deps — the humantime-style parser is 60 lines in-tree.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-13 17:12:27 -07:00
co-authored by Claude Opus 4.7
parent 13fecd798e
commit 4630925040
5 changed files with 517 additions and 2 deletions
+59
View File
@@ -598,6 +598,65 @@ pub async fn call_get_tag_versioned(
))
}
// ── Phase 4b follow-on: TTL client helpers ───────────────────────────
/// Phase 4b follow-on (2026-07-13): attach a TTL sidecar to a stamped
/// tag on a peer. `expires_at_unix == 0` clears any prior sidecar.
///
/// The peer accepts writes even when the stamped tag isn't present
/// yet — the sidecar sticks around and takes effect once the tag
/// lands (`TagStore::set_stamped_expiry` semantics).
pub async fn call_set_tag_expiry(
conn: &Connection,
key: &str,
expires_at_unix: u64,
) -> Result<()> {
let payload = crate::cluster::tags::encode_expiry_record(key, expires_at_unix);
let reply = rpc_call(conn, Method::SetTagExpiry, &payload).await?;
if reply.len() != 1 {
bail!(
"expected single-byte SetTagExpiry reply, got {} bytes",
reply.len()
);
}
match reply[0] {
STREAM_STATUS_OK => Ok(()),
code => match decode_error(code) {
Some(err) => bail!("peer rejected SetTagExpiry: {}", err.describe()),
None => bail!(
"peer replied with unknown byte 0x{:02x} for SetTagExpiry",
code
),
},
}
}
/// Phase 4b follow-on: fetch the TTL sidecar for a stamped tag.
/// Returns `Ok(None)` when no sidecar is present (never expires or
/// no such tag).
pub async fn call_get_tag_expiry(
conn: &Connection,
key: &str,
) -> Result<Option<u64>> {
let reply = rpc_call(conn, Method::GetTagExpiry, key.as_bytes()).await?;
if reply.len() == 1 {
match decode_error(reply[0]) {
Some(ErrorCode::NotFound) => return Ok(None),
Some(err) => bail!("peer replied with error: {}", err.describe()),
None => {}
}
}
if reply.len() != 8 {
bail!(
"expected 8-byte GetTagExpiry reply, got {} bytes",
reply.len()
);
}
Ok(Some(u64::from_le_bytes(
reply.as_slice().try_into().expect("checked length"),
)))
}
// ── Phase 5g: cache metrics client helper ────────────────────────────
/// Fetch the peer's current cache-metrics snapshot.