{"record":{"id":"61399c522896dd2d","repo":"clockworklabs/SpacetimeDB","slug":"invalidinput","errorCode":"InvalidInput","errorMessage":"new epoch is smaller than current epoch","messagePattern":"new epoch is smaller than current epoch","errorType":"validation","errorClass":"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/524b4487d949b61a07d4f39c862d1290259dfd20/crates/commitlog/src/commitlog.rs#L94-L130","documentation":"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.","triggerScenarios":"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.","commonSituations":"Point-in-time restores of the data directory; tests that reset storage but keep epoch counters; replaying recorded epoch sequences from a stale log.","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"],"exampleFix":"// before\nlog.set_epoch(old_epoch)?; // smaller than head's current epoch\n\n// after\nlet next = new_epoch.max(last_applied_epoch);\nlog.set_epoch(next)?;\nlast_applied_epoch = next;","handlingStrategy":"validation","validationCode":"// enforce monotonic epochs at the call site\nlet next = new_epoch.max(last_applied_epoch);\nif next != new_epoch {\n    tracing::warn!(\"refusing to regress epoch {new_epoch}; keeping {last_applied_epoch}\");\n}\nlog.set_epoch(next)?;\nlast_applied_epoch = next;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","commitlog","epoch","invalid-input"],"backgroundTag":"epoch-rollback-rejected","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}