clockworklabs/SpacetimeDB · error · std::io::Error

no space left on device

Error message

no space left on device

What it means

The memory repo emulates a storage device of fixed capacity: Memory::new(total_space) creates a SpaceOnDevice budget shared by all segments, and segments allocate space in PAGE_SIZE (4096-byte) increments. When a write, ftruncate, or fallocate needs more pages than remain, the allocation returns io::ErrorKind::StorageFull with the POSIX ENOSPC message.

Source

Thrown at crates/commitlog/src/repo/mem/segment.rs:252

        if *avail == 0 {
            return Err(enospc());
        }

        let want = size.next_multiple_of(PAGE_SIZE as u64) - storage.alloc;
        let have = want.min(*avail);
        storage.alloc += have;
        *avail -= have;

        if want > have {
            return Err(enospc());
        }

        Ok(())
    }
}

fn enospc() -> io::Error {
    io::Error::new(io::ErrorKind::StorageFull, "no space left on device")
}

#[cfg(feature = "streaming")]
mod async_impls {
    use super::*;

    use std::{
        io::{Read as _, Seek as _, Write as _},
        pin::Pin,
        task::{Context, Poll},
    };

    use tokio::io::{self, AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};

    use crate::stream::{AsyncFsync, AsyncLen, IntoAsyncWriter};

    impl IntoAsyncWriter for Segment {
        type AsyncWriter = tokio::io::BufWriter<Self>;

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Raise the total_space passed to Memory::new, or use Memory::unlimited() when capacity simulation is not the point of the test
  2. Size the budget for worst case: at least max_segment_size per live segment plus overhead
  3. Remove or compact old segments to release their pages before creating new ones
  4. Catch ErrorKind::StorageFull and treat it as a backpressure signal rather than a hard failure

Example fix

// before
let repo = Memory::new(4096); // one page only

// after
// budget for N segments of max_segment_size each
let repo = Memory::new(options.max_segment_size.get() * n_segments as u64 + PAGE_SIZE as u64);
Defensive patterns

Strategy: try-catch

Validate before calling

// If you constructed the Memory repo yourself, track the budget:
// space is Arc<Mutex<u64>>; needed pages = ceil(bytes / PAGE_SIZE)
let pages_needed = (bytes + PAGE_SIZE - 1) / PAGE_SIZE;
if *space.lock().unwrap() < pages_needed as u64 * PAGE_SIZE as u64 {
    return Err(io::Error::new(io::ErrorKind::StorageFull, "insufficient budget"));
}

Type guard

fn is_storage_full(err: &io::Error) -> bool {
    err.kind() == io::ErrorKind::StorageFull
}

Try / catch

match segment.write_all(&buf) {
    Ok(()) => Ok(()),
    Err(e) if is_storage_full(&e) => {
        // release capacity (remove/compact old segments) or apply backpressure
        handle_full_device()?;
        segment.write_all(&buf)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Constructing Memory::new with a small total_space in tests and writing more data than it allows; fallocating Options::max_segment_size for several segments until the budget is exhausted; ftruncate extending a segment beyond available space (see mem.rs tests: ftruncate to 9216 on a 2-page budget fails).

Common situations: Tests that intentionally set a tiny device budget to assert backpressure; forgetting that fallocate/preallocation reserves capacity per segment; sizing total_space below max_segment_size times the expected segment count.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/4c745d500c127cd8. Report an issue: GitHub.