Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions glidefs/src/block/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,
/// 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<u64>,
}
Expand Down Expand Up @@ -360,8 +359,8 @@ async fn create_or_attach_volume(
from: &FromRef,
) -> Response<BoxBody> {
// 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);
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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<dyn object_store::ObjectStore> =
Arc::new(object_store::memory::InMemory::new());

Expand All @@ -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(
Expand All @@ -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"
);
}

Expand Down
39 changes: 19 additions & 20 deletions glidefs/src/block/fence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,28 +66,23 @@ 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,
stored_lease_revision: u64,
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),
Expand Down Expand Up @@ -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]
Expand Down
Loading