clockworklabs/SpacetimeDB · warning · io::Error

InvalidInput

InvalidInput

Error message

refusing to compress mutable segment {head_offset}

What it means

compress_segments refuses to compress the head segment (the segment currently open for appends, identified by its min_tx_offset) because compressing a mutable segment would race with concurrent writes. This is a deterministic usage guard (InvalidInput), not a side-effect failure: nothing was compressed and no state changed.

Source

Thrown at crates/commitlog/src/lib.rs:389

    ///
    /// This method acquires a read lock on this `Commitlog` instance, but
    /// releases it once the compression work starts. Concurrent compression
    /// tasks on the same segment are safe, but external coordination is
    /// required to avoid duplicate work.
    ///
    /// Attempting to compress a segment that is already compressed incurs a
    /// small overhead to open the file and determining its format, but
    /// otherwise does nothing.
    pub fn compress_segments(&self, offsets: &[u64]) -> io::Result<CompressionStats> {
        let (repo, head_offset) = {
            let inner = self.inner.read().unwrap();
            let repo = inner.repo.clone();
            let head_offset = inner.head.min_tx_offset();

            (repo, head_offset)
        };
        if offsets.contains(&head_offset) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("refusing to compress mutable segment {head_offset}"),
            ));
        }
        let mut stats = <_>::default();
        for offset in offsets {
            stats += repo.compress_segment(*offset)?;
        }
        Ok(stats)
    }

    /// Remove all data from the log and reopen it.
    ///
    /// Log segments are deleted starting from the newest. As multiple segments
    /// cannot be deleted atomically, the log may not be completely empty if
    /// the method returns an error.
    ///
    /// Note that the method consumes `self` to ensure the log is not modified

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Drop the head segment from the list: head is the maximum of existing_segment_offsets(); pass only offsets < head
  2. Roll to a new segment first (write past max_segment_size or lower it) so the target segment becomes immutable, then compress it
  3. Treat the refusal as a no-op signal and skip that offset

Example fix

// before
let offsets = log.existing_segment_offsets()?;
log.compress_segments(&offsets)?; // last entry is the mutable head -> refused

// after
let mut offsets = log.existing_segment_offsets()?;
offsets.pop(); // discard the mutable head segment
log.compress_segments(&offsets)?;
Defensive patterns

Strategy: validation

Validate before calling

let mut offsets = log.existing_segment_offsets()?;
if let Some(&head) = offsets.last() {
    offsets.retain(|&o| o != head); // the head segment is still mutable
}
log.compress_segments(&offsets)?;

Type guard

fn is_mutable_segment_refusal(e: &io::Error) -> bool {
    e.kind() == io::ErrorKind::InvalidInput
        && e.to_string().contains("refusing to compress mutable segment")
}

Try / catch

match log.compress_segments(&offsets) {
    Ok(stats) => { /* ... */ }
    Err(e) if is_mutable_segment_refusal(&e) => { /* skip the head segment, not an error */ }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling log.compress_segments(&offsets) with a list containing the current head segment's base offset. The typical source is passing the unfiltered result of log.existing_segment_offsets(), whose last entry is exactly the mutable head segment.

Common situations: A maintenance job that 'compresses all segments' by listing the directory; compressing right after startup before the log has rolled to a new segment.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/347b72d397b5ee7f. Report an issue: GitHub.