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.
    ///
    /// # Panics

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Only advance the epoch: call set_epoch with a value greater than or equal to the current one (equal is a no-op)
  2. When restoring older storage, reset persisted epoch state to match the restored data
  3. 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

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


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