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 — sourcegrafos_std::block::{BlockBuilder, BlockLease}— block storage acquisition — sourcegrafos_fence::{FenceGuard, FenceEpoch, Fenced}— superblock epoch protection — sourcegrafos_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 succeedUser B mounts at epoch 1 -> epoch advancesUser 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 writelet mut fh = fs.create("/results/output.bin")?;fs.write(&mut fh, &computed_data)?;fs.close(fh)?;
// Read backlet 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 mountslet 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 — rejectedassert!(guard.check(epoch_a).is_err());
// User B writes — acceptedassert!(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/writecalls fail withFabricError::Disconnected. The filesystem is irrecoverable. This is by design: the data was temporary. - Stale epoch write:
FenceGuard::check()returnsStaleEpochError. 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
RenewalSummaryfromtick()— remaining TTL on block leasesfs.readdir("/")— current filesystem contentsFenceGuardepoch value — mount generation for debugging concurrent access
Variations
- Multi-lease striping: pass multiple
BlockLeaseinstances toFabricFs::format()for parallel I/O across fabric nodes - Tiered retention: short TTL for scratch space, longer TTL for staging areas, using different
RenewalPolicyconfigurations - Shared read-only: after the writer finishes, a separate reader mounts with a fresh
FenceGuardepoch and reads without writing (no stale-write risk) - Persistent subset: copy important results to a durable
grafos_kv::FabricKvStorebefore letting the filesystem expire