Skip to content

Recipe 24: A Shared Filesystem That Disappears When Nobody Is Using It

Situation

Teams need temporary shared storage for collaborative work: build artifacts, data processing staging areas, experiment outputs. Traditional shared filesystems (NFS, CIFS) require provisioning and decommissioning. An abandoned share wastes storage until someone notices and cleans it up.

You want a filesystem that exists only while someone is actively using it. When the last user walks away and stops renewing, the block leases expire and the filesystem vanishes. No decommissioning workflow. No orphaned mounts.

What You Build

A grafos_fs::FabricFs formatted over BlockLease instances. Files and directories live on leased block storage. FenceGuard protects the superblock against concurrent format/mount races. RenewalManager keeps block leases alive while the filesystem is in use. When all handles drop and renewal stops, leases expire and the filesystem ceases to exist.

Building Blocks

  • grafos_fs::{FabricFs, OpenFlags, DirEntry} — filesystem operations — source
  • grafos_std::block::{BlockBuilder, BlockLease} — block storage acquisition — source
  • grafos_fence::{FenceGuard, FenceEpoch, Fenced} — superblock epoch protection — source
  • grafos_leasekit::{RenewalManager, RenewalPolicy} — block lease renewal — source

Design

Block Lease Pool

The filesystem is backed by one or more BlockLease instances. Each lease provides a contiguous block range. Multiple leases can be combined for larger filesystems. The on-block format uses a superblock (magic "GFFS"), free block bitmap, inode table, and data blocks.

FabricFs Format and Mount

FabricFs::format(leases) writes the superblock and initializes the on-block structures. After formatting, the filesystem is ready for file operations: create, open, read, write, seek, readdir, unlink, close.

FenceGuard Superblock Protection

Multiple users mounting the same block leases concurrently risks superblock corruption. A FenceGuard tracks the current epoch. Each mount increments the epoch. Writes from a stale epoch are rejected with StaleEpochError.

User A mounts at epoch 0 -> writes succeed
User B mounts at epoch 1 -> epoch advances
User A writes -> StaleEpochError (epoch 0 < current 1)

This is not multi-writer concurrency control (which would need a distributed lock). It is stale-write rejection: the last mounter wins, and prior mounters fail closed.

Renewal-Driven Lifetime

Register each BlockLease with RenewalManager. While any user is actively calling tick(), leases renew and the filesystem persists. When the last user stops, leases expire. The blocks return to the fabric. The filesystem is gone.

Disappearance Semantics

There is no explicit “unmount” or “delete filesystem” operation. Disappearance is a consequence of lease expiry. This is the key property: cleanup is not a step that can be forgotten or fail. It is the default outcome of inaction.

Walkthrough (Implementation Sketch)

Core grafOS API Path

The filesystem is a block lease, a FabricFs mount, and a renewal entry keyed by the actual lease id:

use grafos_fs::{FabricFs, OpenFlags};
use grafos_fence::{FenceEpoch, FenceGuard};
use grafos_leasekit::{RenewalManager, RenewalPolicy};
use grafos_std::block::BlockBuilder;
let lease = BlockBuilder::new()
.min_blocks(2048)
.lease_secs(1800)
.acquire()?;
let lease_id = lease.lease_id();
let expires_at = lease.expires_at_unix_secs();
let mut renewals = RenewalManager::new();
renewals.register(lease_id, expires_at, RenewalPolicy::default());
let mut fs = FabricFs::format(vec![lease])?;
let mut fh = fs.create("/results/output.bin")?;
fs.write(&mut fh, b"artifact")?;
fs.close(fh)?;
let guard = FenceGuard::new(FenceEpoch::zero());
let summary = renewals.tick(now);
# let _ = (fs, guard, summary);
# Ok::<(), grafos_std::FabricError>(())

1. Lease Block Storage

use grafos_std::block::BlockBuilder;
use grafos_leasekit::{RenewalManager, RenewalPolicy};
let lease = BlockBuilder::new().min_blocks(2048).lease_secs(1800).acquire()?;
let lease_id = lease.lease_id();
let expires_at = lease.expires_at_unix_secs();
let mut renewals = RenewalManager::new();
renewals.register(lease_id, expires_at, RenewalPolicy::default());

2. Format the Filesystem

use grafos_fs::{FabricFs, OpenFlags};
let mut fs = FabricFs::format(vec![lease])?;

This writes the GFFS superblock, initializes the bitmap, and prepares the inode table.

3. Create, Write, and Read Files

// Create and write
let mut fh = fs.create("/results/output.bin")?;
fs.write(&mut fh, &computed_data)?;
fs.close(fh)?;
// Read back
let fh = fs.open("/results/output.bin", OpenFlags::Read)?;
let mut buf = vec![0u8; computed_data.len()];
let n = fs.read(&fh, &mut buf)?;
assert_eq!(&buf[..n], &computed_data);

4. Concurrent Mount Protection with FenceGuard

use grafos_fence::{FenceGuard, FenceEpoch};
let mut guard = FenceGuard::new(FenceEpoch::zero());
// First user mounts
let epoch_a = guard.advance();
// ... user A works with the filesystem ...
// Second user mounts (e.g., after first user went idle)
let epoch_b = guard.advance();
// User A tries to write — rejected
assert!(guard.check(epoch_a).is_err());
// User B writes — accepted
assert!(guard.check(epoch_b).is_ok());

5. Keep Alive While In Use

// In the user's work loop:
loop {
do_work(&mut fs)?;
let summary = renewals.tick(unix_time_secs());
if !summary.near_expiry.is_empty() {
// Block lease is close to expiry — checkpoint or stop writing
break;
}
}

6. Walk Away and the Filesystem Vanishes

When the user finishes and drops the FabricFs handle, stop calling renewals.tick(). The block leases expire at now + ttl. The fabric reclaims the blocks. Any subsequent attempt to access the blocks will fail. The filesystem is gone, with no cleanup step.

Failure Modes

  • Block lease expires while files are open: all subsequent read/write calls fail with FabricError::Disconnected. The filesystem is irrecoverable. This is by design: the data was temporary.
  • Stale epoch write: FenceGuard::check() returns StaleEpochError. The writer must re-mount (re-read superblock, get current epoch) before continuing.
  • Insufficient blocks: BlockBuilder::acquire() fails if the fabric cannot provide enough block storage. Request fewer blocks or wait for capacity.

Observability

  • RenewalSummary from tick() — remaining TTL on block leases
  • fs.readdir("/") — current filesystem contents
  • FenceGuard epoch value — mount generation for debugging concurrent access

Variations

  • Multi-lease striping: pass multiple BlockLease instances to FabricFs::format() for parallel I/O across fabric nodes
  • Tiered retention: short TTL for scratch space, longer TTL for staging areas, using different RenewalPolicy configurations
  • Shared read-only: after the writer finishes, a separate reader mounts with a fresh FenceGuard epoch and reads without writing (no stale-write risk)
  • Persistent subset: copy important results to a durable grafos_kv::FabricKvStore before letting the filesystem expire