diff --git a/glidefs/src/block/api.rs b/glidefs/src/block/api.rs index 881ecc4..0675681 100644 --- a/glidefs/src/block/api.rs +++ b/glidefs/src/block/api.rs @@ -61,16 +61,15 @@ pub struct CreateVolumeRequest { /// placement generation). When `> 0`, the attach is admitted only if this /// generation is `>=` the volume's stored generation; a strictly-newer /// generation fences an older holder out (see [`crate::block::fence`]). - /// Omitted / `0` = the caller does not participate in the high half. + /// Omitted / `0` on both halves is Grant only against an unfenced volume. #[serde(default)] pub generation: Option, /// Single-attach fencing token, low half: the node-lease claim revision. /// Orders same-`node_id` incarnations (a node-death re-fork does NOT advance /// the orchestrator [`Self::generation`], so this is what fences it). The /// fence compares the composite `(generation, lease_revision)`. Omitted / - /// `0` = the caller does not participate in the low half. The back-compat - /// bypass requires BOTH halves to be 0; a caller sending only a - /// `lease_revision` DOES participate. (See [`crate::block::fence`].) + /// `0` on both halves is Grant only against an unfenced volume. A caller + /// sending only a `lease_revision` participates. (See [`crate::block::fence`].) #[serde(default)] pub lease_revision: Option, } @@ -360,8 +359,8 @@ async fn create_or_attach_volume( from: &FromRef, ) -> Response { // Single-attach fencing token: high half = orchestrator placement - // generation, low half = node-lease claim revision. 0/0 = the caller does - // not participate in fencing (back-compat bypass); a lease-only token still + // generation, low half = node-lease claim revision. 0/0 is Grant only + // against an unfenced volume; a lease-only token still // participates. let my_gen = req.generation.unwrap_or(0); let my_lease = req.lease_revision.unwrap_or(0); @@ -490,7 +489,8 @@ async fn create_or_attach_volume( // Attach-time fence + seize, BEFORE persisting the index, registering // the device, or serving any I/O. A rejected attach has uploaded zero // data packs, so there is nothing to orphan — this is what prevents - // the at-sync orphaned-packs data loss. Skipped for the 0/0 bypass. + // the at-sync orphaned-packs data loss. (0,0) is Grant only + // against an unfenced volume. match router.enforce_attach_fence(name, my_gen, my_lease).await { Ok(crate::block::fence::Fence::Grant) => {} Ok(crate::block::fence::Fence::Reject) => { @@ -1983,10 +1983,10 @@ mod tests { ); } - /// Back-compat: a caller that does not send a generation (the gen-0 bypass) - /// is never fenced, even against a volume already owned at a high generation. + /// A caller that omits generation (token 0,0) is admitted only against an + /// unfenced volume. Once the volume holds a real token, (0,0) is 409. #[tokio::test] - async fn test_attach_without_generation_bypasses_fence() { + async fn test_attach_without_generation_is_fenced() { let shared: Arc = Arc::new(object_store::memory::InMemory::new()); @@ -2000,7 +2000,6 @@ mod tests { ) .await; - // A legacy caller (no generation field) attaches on a fresh node → granted. let temp_b = TempDir::new().unwrap(); let node_b = create_test_router_with_store(&temp_b, Arc::clone(&shared)).await; let resp = request( @@ -2012,8 +2011,8 @@ mod tests { .await; assert_eq!( resp.status(), - StatusCode::CREATED, - "the gen-0 bypass must not be fenced" + StatusCode::CONFLICT, + "a (0,0) caller must not attach a volume already owned at gen 9" ); } diff --git a/glidefs/src/block/fence.rs b/glidefs/src/block/fence.rs index c8629ba..261e687 100644 --- a/glidefs/src/block/fence.rs +++ b/glidefs/src/block/fence.rs @@ -66,18 +66,16 @@ pub fn attach_fence(stored_token: u128, my_token: u128) -> Fence { } } -/// Call-site wrapper applying the back-compat bypass around [`attach_fence`], -/// composing the caller's `(generation, lease_revision)` against the stored -/// `(stored_generation, stored_lease_revision)`. +/// Call-site wrapper around [`attach_fence`], composing the caller's +/// `(generation, lease_revision)` against the stored token. /// -/// Fencing engages only when the caller opts in with a non-zero token. A caller -/// presenting `generation == 0 && lease_revision == 0` (a legacy instd that -/// sends neither, or any non-participating client) is always granted and never -/// bumps the stored token. Note the bypass requires BOTH halves to be zero: a -/// caller that sends only a `lease_revision` (generation 0, lease > 0) DOES -/// participate and is fenced on the composite. Roll-out is therefore one-way per -/// volume: once a volume has been stamped with any non-zero token, only callers -/// presenting a high-enough composite are admitted. +/// A `(0, 0)` caller is only admitted against an unfenced volume +/// (stored token also `(0, 0)` — first attach / homelab with no +/// orchestrator generation). Once the volume holds a real token, +/// `(0, 0)` is `Reject`: the same machine as +/// `attach_fence(stored, compose(0, 0))`. A caller that sends only a +/// `lease_revision` (generation 0, lease > 0) participates and is +/// ordered on the composite. #[must_use] pub fn fence_attach( stored_generation: u64, @@ -85,9 +83,6 @@ pub fn fence_attach( my_generation: u64, my_lease_revision: u64, ) -> Fence { - if my_generation == 0 && my_lease_revision == 0 { - return Fence::Grant; - } attach_fence( compose_token(stored_generation, stored_lease_revision), compose_token(my_generation, my_lease_revision), @@ -166,13 +161,17 @@ mod tests { } #[test] - fn zero_token_bypass_always_grants() { - // A fully non-participating caller (generation == 0 AND - // lease_revision == 0) is never fenced, even against an already-fenced - // volume — this is the back-compat bypass. - assert_eq!(fence_attach(5, 0, 0, 0), Fence::Grant); + fn zero_token_does_not_bypass_a_fenced_volume() { + // First attach against an unfenced volume is still Grant. assert_eq!(fence_attach(0, 0, 0, 0), Fence::Grant); - assert_eq!(fence_attach(9, 4, 0, 0), Fence::Grant); + // Once a volume holds a real token, (0,0) must not sneak in — + // that is the same machine as attach_fence(stored, compose(0,0)). + assert_eq!(fence_attach(5, 0, 0, 0), Fence::Reject); + assert_eq!(fence_attach(9, 4, 0, 0), Fence::Reject); + assert_eq!( + fence_attach(9, 4, 0, 0), + attach_fence(compose_token(9, 4), compose_token(0, 0)) + ); } #[test]