From 9c3c92634a04870a593cf7116646ae5cdb177e3c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 12:28:07 +0200 Subject: [PATCH] Require an atomic push_all from every Rust backing store The default CacheBackingStore::push_all deleted every row and then pushed one by one, so a failing push left the store emptied or partial while put_prepared_batch claimed all-or-nothing. The default is deleted: push_all is a required method documented as one atomic replace, and a compile_fail doctest pins that a store without it does not compile. Every in-tree implementor (single-file, both redb stores, the test store, Epiphany's runtime store) already supplies its own. Co-Authored-By: Claude Opus 5 --- packages/cultcache-rs/src/lib.rs | 35 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/cultcache-rs/src/lib.rs b/packages/cultcache-rs/src/lib.rs index fddf6ef8..ed9cae32 100644 --- a/packages/cultcache-rs/src/lib.rs +++ b/packages/cultcache-rs/src/lib.rs @@ -309,16 +309,31 @@ pub trait CacheBackingStore: Send { fn push(&mut self, entry: &CultCacheEnvelope) -> Result<()>; fn delete(&mut self, entry: &CultCacheEnvelope) -> Result<()>; - fn push_all(&mut self, entries: &[CultCacheEnvelope], _options: PushAllOptions) -> Result<()> { - let existing = self.pull_all()?; - for entry in existing { - self.delete(&entry)?; - } - for entry in entries { - self.push(entry)?; - } - Ok(()) - } + /// Replaces everything the store holds with `entries` in one atomic step: + /// on `Err` the store still holds exactly what it held before. A cache + /// batch (`put_prepared_batch`) is all-or-nothing only through this. There + /// is no default, because a replace assembled from `delete` and `push` + /// calls leaves the store emptied or partial when one of them fails; a + /// store without an atomic replace does not compile: + /// + /// ```compile_fail,E0046 + /// use cultcache_rs::{CacheBackingStore, CultCacheEnvelope}; + /// + /// struct PerRecordStore; + /// + /// impl CacheBackingStore for PerRecordStore { + /// fn pull_all(&self) -> anyhow::Result> { + /// Ok(Vec::new()) + /// } + /// fn push(&mut self, _entry: &CultCacheEnvelope) -> anyhow::Result<()> { + /// Ok(()) + /// } + /// fn delete(&mut self, _entry: &CultCacheEnvelope) -> anyhow::Result<()> { + /// Ok(()) + /// } + /// } + /// ``` + fn push_all(&mut self, entries: &[CultCacheEnvelope], options: PushAllOptions) -> Result<()>; } #[derive(Clone, Debug, PartialEq, Eq)]