clockworklabs/SpacetimeDB · error · io::Error
InvalidInput
InvalidInput
Error message
new epoch is smaller than current epoch
What it means
Commitlog epochs are monotonic: `set_epoch` accepts the current epoch (a no-op) or a larger one, and rejects anything smaller with InvalidInput. This guards against replaying older coordinator state over newer committed data. A regression usually means restored-old-storage combined with newer in-memory state, or a bug in how the epoch value is derived.
Source
Thrown at crates/commitlog/src/commitlog.rs:112
/// Get the current epoch.
///
/// See also: [`Commit::epoch`].
pub fn epoch(&self) -> u64 {
self.head.commit.epoch
}
/// Update the current epoch.
///
/// Does nothing if the given `epoch` is equal to the current epoch.
///
/// # Errors
///
/// If `epoch` is smaller than the current epoch, an error of kind
/// [`io::ErrorKind::InvalidInput`] is returned.
pub fn set_epoch(&mut self, epoch: u64) -> io::Result<()> {
if epoch < self.head.epoch() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"new epoch is smaller than current epoch",
));
}
self.head.set_epoch(epoch);
Ok(())
}
/// Force the currently active segment to be flushed to storage.
///
/// Using a filesystem backend, this means to call `fsync(2)`.
///
/// **Note** that this does not flush the buffered data from calls to
/// [Self::commit], it only instructs the underlying storage to flush its
/// buffers. Call [Self::flush] prior to this method to ensure data from
/// all previous [Self::commit] calls is flushed to the underlying storage.
///
/// # PanicsView on GitHub (pinned to 524b4487d9)
Solutions
- Only advance the epoch: call set_epoch with a value greater than or equal to the current one (equal is a no-op)
- When restoring older storage, reset persisted epoch state to match the restored data
- Audit the epoch source — it must be derived monotonically, never replayed from a stale record
Example fix
// before log.set_epoch(old_epoch)?; // smaller than head's current epoch // after let next = new_epoch.max(last_applied_epoch); log.set_epoch(next)?; last_applied_epoch = next;
Defensive patterns
Strategy: validation
Validate before calling
// enforce monotonic epochs at the call site
let next = new_epoch.max(last_applied_epoch);
if next != new_epoch {
tracing::warn!("refusing to regress epoch {new_epoch}; keeping {last_applied_epoch}");
}
log.set_epoch(next)?;
last_applied_epoch = next; Prevention
- Persist the last-applied epoch with the data it guards; restore them together
- Derive epochs from a monotonic source — never from wall-clock resets or stale logs
- In tests, reset storage and epoch state as one unit
When it happens
Trigger: Calling `commitlog.set_epoch(e)` where `e` is smaller than the head's current epoch — replaying an older epoch sequence, or restoring old storage while the caller tracks a newer epoch.
Common situations: Point-in-time restores of the data directory; tests that reset storage but keep epoch counters; replaying recorded epoch sequences from a stale log.
Related errors
- InvalidInput
- InvalidInput
- invalid transaction offset {}, expected {}
- error reading commit header: {e}
- failed to read {} bytes of commit payload: {}
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/61399c522896dd2d.
Report an issue: GitHub.