Phase 3c + 3e: stamped tags + namespaced ref keys — closes Phase 3
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 20s
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 20s
Ships the last two pieces from the arch doc's Phase 3 scope for the
cargo-cache use case:
## 3c: Stamped tags (CRDT-merge on PutTag)
Mirror of Phase 3a/3b for TagStore. Two concurrent `claw-cargo pin`
calls on the same tag now race deterministically instead of silently
clobbering.
* `StampedTagValue` — same 48-byte (value, clock, node) tuple as
StampedRef.
* `TagStore::put_stamped(key, StampedTagValue) -> TagPutOutcome`
and `TagStore::get_stamped(key)` — data lives under `tags-v2/`
(separate from `tags/` for cutover safety).
* New wire methods `PutTagVersioned = 0x18` +
`GetTagVersioned = 0x19`.
* `call_put_tag_versioned` / `call_get_tag_versioned` client
helpers.
* `claw-cargo pin` now writes stamped tags. Concurrent pin gets
AlreadyExists and moves on (blob content is content-addressed so
both winners agree on the payload).
## 3e: Namespaced ref keys
Opt-in `--namespace <slug>` on peer-facing subcommands. When set,
the ref key becomes `blake3("clawstor.ns.v1" || namespace || fp)`
so two runners on different namespaces (`clawverse/main` vs
`clawverse/pr-42`) don't collide on the same fingerprint. Empty
namespace = pre-3e behavior, so this is 100% backward compat.
* `refs::namespaced_ref_key(namespace, fingerprint) -> RefKey`
primitive.
* `PeerArgs::namespace: Option<String>` CLI flag flows through to
`cmd_status`, `cmd_prefetch`, `cmd_build`.
* `peer_lookup` now takes a `RefKey` directly (was `&Fingerprint`)
so the namespace resolution stays in the caller — the daemon
never sees "namespace" as a concept.
## 3d: Deferred
Full vector clocks per namespace are noted in the arch doc as a
Phase-3 goal; scalar wall-clock (clock + node stamp) is sufficient
for the cargo-cache use case (single-key LWW merge). NTP-synced
runners see monotonic ordering; skewed runners lose an ordering
but the CRDT semantics still guarantee no data corruption. Full VC
is deferred to a future phase.
+9 tests, 280 total (baseline +8: 7 unit + 1 e2e over real QUIC).
This commit is contained in:
@@ -1364,3 +1364,74 @@ async fn end_to_end_put_ref_versioned_merges_and_rejects() {
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
acc.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn end_to_end_put_tag_versioned_merges_and_rejects() {
|
||||
// Phase 3c end-to-end: two writers publish stamped tags for the
|
||||
// same key. Higher (clock, node) wins.
|
||||
use crate::cluster::refs::node_stamp_for;
|
||||
use crate::cluster::tags::StampedTagValue;
|
||||
|
||||
let (id_a, id_b) = NodeIdentity::generate_test_pair("a", "b").unwrap();
|
||||
let (_tmp_a, router_a) = router_with_full_stack("a", next_port()).await;
|
||||
|
||||
let server_a = QuicServer::bind(loopback(0), id_a).unwrap();
|
||||
let addr = server_a.local_addr().unwrap();
|
||||
let ra = router_a.clone();
|
||||
let acc = tokio::spawn(async move {
|
||||
while let Some(Ok(conn)) = server_a.accept().await {
|
||||
let r = ra.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = serve_connection(conn, r).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
let client = QuicClient::new(loopback(0), id_b).unwrap();
|
||||
let conn = client.connect(addr, "a").await.unwrap();
|
||||
|
||||
let key = "clawverse:main:latest";
|
||||
let node_x = node_stamp_for("runner-x");
|
||||
let node_y = node_stamp_for("runner-y");
|
||||
|
||||
assert_eq!(call_get_tag_versioned(&conn, key).await.unwrap(), None);
|
||||
|
||||
let first = StampedTagValue {
|
||||
value: [0x11; 32],
|
||||
clock: 10,
|
||||
node: node_x,
|
||||
};
|
||||
assert!(call_put_tag_versioned(&conn, key, &first).await.unwrap());
|
||||
assert_eq!(
|
||||
call_get_tag_versioned(&conn, key).await.unwrap(),
|
||||
Some(first)
|
||||
);
|
||||
|
||||
// Older clock → rejected.
|
||||
let older = StampedTagValue {
|
||||
value: [0x22; 32],
|
||||
clock: 9,
|
||||
node: node_y,
|
||||
};
|
||||
assert!(!call_put_tag_versioned(&conn, key, &older).await.unwrap());
|
||||
assert_eq!(
|
||||
call_get_tag_versioned(&conn, key).await.unwrap(),
|
||||
Some(first)
|
||||
);
|
||||
|
||||
// Higher clock always merges.
|
||||
let latest = StampedTagValue {
|
||||
value: [0x33; 32],
|
||||
clock: 100,
|
||||
node: node_x,
|
||||
};
|
||||
assert!(call_put_tag_versioned(&conn, key, &latest).await.unwrap());
|
||||
assert_eq!(
|
||||
call_get_tag_versioned(&conn, key).await.unwrap(),
|
||||
Some(latest)
|
||||
);
|
||||
|
||||
conn.close(quinn::VarInt::from_u32(0), b"done");
|
||||
client.shutdown().await;
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
acc.abort();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user