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 modifiedView on GitHub (pinned to 524b4487d9)
Solutions
- Drop the head segment from the list: head is the maximum of existing_segment_offsets(); pass only offsets < head
- Roll to a new segment first (write past max_segment_size or lower it) so the target segment becomes immutable, then compress it
- 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
- Compress only sealed segments: everything except the maximum offset from existing_segment_offsets()
- Run compression from a maintenance path, never on the hot write path
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.