{"record":{"id":"09cae406d8dd2b08","repo":"clockworklabs/SpacetimeDB","slug":"new-epoch-is-smaller-than-current-epoch","errorCode":null,"errorMessage":"new epoch is smaller than current epoch","messagePattern":"new epoch is smaller than current epoch","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/commitlog/src/commitlog.rs","lineNumber":112,"sourceCode":"\n    /// Get the current epoch.\n    ///\n    /// See also: [`Commit::epoch`].\n    pub fn epoch(&self) -> u64 {\n        self.head.commit.epoch\n    }\n\n    /// Update the current epoch.\n    ///\n    /// Does nothing if the given `epoch` is equal to the current epoch.\n    ///\n    /// # Errors\n    ///\n    /// If `epoch` is smaller than the current epoch, an error of kind\n    /// [`io::ErrorKind::InvalidInput`] is returned.\n    pub fn set_epoch(&mut self, epoch: u64) -> io::Result<()> {\n        if epoch < self.head.epoch() {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidInput,\n                \"new epoch is smaller than current epoch\",\n            ));\n        }\n        self.head.set_epoch(epoch);\n        Ok(())\n    }\n\n    /// Force the currently active segment to be flushed to storage.\n    ///\n    /// Using a filesystem backend, this means to call `fsync(2)`.\n    ///\n    /// **Note** that this does not flush the buffered data from calls to\n    /// [Self::commit], it only instructs the underlying storage to flush its\n    /// buffers. Call [Self::flush] prior to this method to ensure data from\n    /// all previous [Self::commit] calls is flushed to the underlying storage.\n    ///\n    /// # Panics","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/9e0d92412ff2248f401a8ad12d535f2b5ac30912/crates/commitlog/src/commitlog.rs#L94-L130","documentation":"Commitlog::set_epoch enforces that epochs never regress: the epoch is a monotonically increasing term number (leader fencing in distributed deployments), and passing a value below the current head epoch returns io::ErrorKind::InvalidInput with this message without changing anything. Setting the same epoch again is an accepted no-op.","triggerScenarios":"Calling commitlog.set_epoch(n) with n smaller than the epoch already recorded in the head segment - e.g. replaying stale coordinator state, restoring an older data directory alongside newer epoch bookkeeping, or computing the next epoch from a cached value.","commonSituations":"Leader/failover logic that recomputes epochs from stale config; restore-from-backup followed by re-running the promotion sequence; test harnesses resetting epoch to 0 against an existing log.","solutions":["Always derive the next epoch from the log's current epoch (current + 1 or higher) instead of a stored constant.","After restoring an old data directory, bump epochs past any previously used value rather than replaying old numbers.","On this error, read the current head epoch from the log and retry with a strictly larger value."],"exampleFix":"// before\ncommitlog.set_epoch(previous_epoch)?;\n\n// after\nlet next = current_epoch.max(previous_epoch) + 1;\ncommitlog.set_epoch(next)?;","handlingStrategy":"validation","validationCode":"// Guard before calling set_epoch: epochs must never decrease\nif new_epoch < current_epoch {\n    anyhow::bail!(\"refusing to regress epoch {current_epoch} -> {new_epoch}\");\n}\ncommitlog.set_epoch(new_epoch)?;","typeGuard":null,"tryCatchPattern":"match commitlog.set_epoch(epoch) {\n    Ok(()) => {}\n    Err(e) if e.kind() == io::ErrorKind::InvalidInput => {\n        // recompute from the log's current epoch and retry once with a strictly larger value\n        let bumped = current_epoch + 1;\n        commitlog.set_epoch(bumped)?;\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Keep one source of truth for the current epoch (the log head) and always derive the next as head + 1.","Never initialize epochs from stale config after a restore - query the log first.","In tests, use a fresh commitlog directory when resetting epochs to zero."],"tags":["rust","commitlog","epoch","distributed","validation"],"backgroundTag":"epoch-regression","analyzedSha":"9e0d92412ff2248f401a8ad12d535f2b5ac30912","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}