diff --git a/src/indexed_snapshot.rs b/src/indexed_snapshot.rs index 146fc0d..749a0c6 100644 --- a/src/indexed_snapshot.rs +++ b/src/indexed_snapshot.rs @@ -14,15 +14,15 @@ use crate::PrefixBound; use crate::{Bound, IndexList, Map, Path, Strategy}; /// `IndexedSnapshotMap` works like a `SnapshotMap` but has a secondary index -pub struct IndexedSnapshotMap<'a, K, T, I> { +pub struct IndexedSnapshotMap<'a, K, T, I, S = Strategy> { pk_namespace: &'a [u8], - primary: SnapshotMap<'a, K, T>, + primary: SnapshotMap<'a, K, T, S>, /// This is meant to be read directly to get the proper types, like: /// map.idx.owner.items(...) pub idx: I, } -impl<'a, K, T, I> IndexedSnapshotMap<'a, K, T, I> { +impl<'a, K, T, I, S> IndexedSnapshotMap<'a, K, T, I, S> { /// Examples: /// /// ```rust @@ -48,7 +48,7 @@ impl<'a, K, T, I> IndexedSnapshotMap<'a, K, T, I> { pk_namespace: &'a str, checkpoints: &'a str, changelog: &'a str, - strategy: Strategy, + strategy: S, indexes: I, ) -> Self { IndexedSnapshotMap { diff --git a/src/lib.rs b/src/lib.rs index a91f096..51e56db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,7 @@ pub use path::Path; #[cfg(feature = "iterator")] pub use prefix::{range_with_prefix, Prefix}; #[cfg(feature = "iterator")] -pub use snapshot::{SnapshotItem, SnapshotMap, Strategy}; +pub use snapshot::{IntervalStrategy, SnapshotItem, SnapshotMap, Strategy}; // cw_storage_macro reexports #[cfg(all(feature = "iterator", feature = "macro"))] diff --git a/src/snapshot/interval_strategy.rs b/src/snapshot/interval_strategy.rs new file mode 100644 index 0000000..ef5b7e3 --- /dev/null +++ b/src/snapshot/interval_strategy.rs @@ -0,0 +1,116 @@ +use cosmwasm_std::{Order, StdResult, Storage}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::{Bound, KeyDeserialize, Map, Prefixer, PrimaryKey}; + +use super::{ChangeSet, SnapshotStrategy}; + +/// A SnapshotStrategy that takes a snapshot only if at least the specified interval has passed. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct IntervalStrategy { + /// The interval to archive snapshots at. If the time or number of blocks since the last changelog + /// entry is greater than this interval, a new snapshot will be created. + pub interval: u64, +} + +impl IntervalStrategy { + /// Create a new IntervalStrategy with the given interval. + pub const fn new(interval: u64) -> Self { + Self { interval } + } +} + +impl<'a, K, T> SnapshotStrategy<'a, K, T> for IntervalStrategy +where + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize, +{ + fn assert_checkpointed( + &self, + _store: &dyn Storage, + _checkpoints: &Map<'a, u64, u32>, + _height: u64, + ) -> StdResult<()> { + Ok(()) + } + + fn should_archive( + &self, + store: &dyn Storage, + _checkpoints: &Map<'a, u64, u32>, + changelog: &Map<'a, (K, u64), ChangeSet>, + key: &K, + height: u64, + ) -> StdResult { + let last_height = height.saturating_sub(self.interval); + + // Check if there is a changelog entry since the last interval + let changelog_entry = changelog + .prefix(key.clone()) + .range_raw( + store, + Some(Bound::inclusive(last_height)), + None, + Order::Ascending, + ) + .next(); + + Ok(changelog_entry.is_none()) + } +} + +#[cfg(test)] +mod tests { + use crate::snapshot::Snapshot; + + use super::*; + use cosmwasm_std::testing::MockStorage; + + type TestSnapshot = Snapshot<'static, &'static str, u64, IntervalStrategy>; + const INTERVAL_5: TestSnapshot = Snapshot::new( + "interval_5__check", + "interval_5__change", + IntervalStrategy::new(5), + ); + + const DUMMY_KEY: &str = "dummy"; + + #[test] + fn should_archive() { + let mut store = MockStorage::new(); + + // Should archive first save since there is no previous changelog entry. + assert_eq!(INTERVAL_5.should_archive(&store, &DUMMY_KEY, 0), Ok(true)); + + // Store changelog entry + INTERVAL_5 + .write_changelog(&mut store, DUMMY_KEY, 0, None) + .unwrap(); + + // Should not archive again + assert_eq!(INTERVAL_5.should_archive(&store, &DUMMY_KEY, 0), Ok(false)); + + // Should archive once interval has passed + assert_eq!(INTERVAL_5.should_archive(&store, &DUMMY_KEY, 6), Ok(true)); + + // Store changelog entry + INTERVAL_5 + .write_changelog(&mut store, DUMMY_KEY, 6, None) + .unwrap(); + + // Should not archive again + assert_eq!(INTERVAL_5.should_archive(&store, &DUMMY_KEY, 6), Ok(false)); + + // Should not archive before interval + assert_eq!( + INTERVAL_5.should_archive(&store, &DUMMY_KEY, 6 + 5), + Ok(false) + ); + + // Should archive once interval has passed + assert_eq!( + INTERVAL_5.should_archive(&store, &DUMMY_KEY, 6 + 5 + 1), + Ok(true) + ); + } +} diff --git a/src/snapshot/item.rs b/src/snapshot/item.rs index 633f073..ba3eee1 100644 --- a/src/snapshot/item.rs +++ b/src/snapshot/item.rs @@ -6,16 +6,18 @@ use cosmwasm_std::{StdError, StdResult, Storage}; use crate::snapshot::{ChangeSet, Snapshot}; use crate::{Item, Map, Strategy}; +use super::SnapshotStrategy; + /// Item that maintains a snapshot of one or more checkpoints. /// We can query historical data as well as current state. /// What data is snapshotted depends on the Strategy. -pub struct SnapshotItem<'a, T> { +pub struct SnapshotItem<'a, T, S = Strategy> { primary: Item<'a, T>, changelog_namespace: &'a str, - snapshots: Snapshot<'a, (), T>, + snapshots: Snapshot<'a, (), T, S>, } -impl<'a, T> SnapshotItem<'a, T> { +impl<'a, T, S> SnapshotItem<'a, T, S> { /// Example: /// /// ```rust @@ -31,7 +33,7 @@ impl<'a, T> SnapshotItem<'a, T> { storage_key: &'a str, checkpoints: &'a str, changelog: &'a str, - strategy: Strategy, + strategy: S, ) -> Self { SnapshotItem { primary: Item::new(storage_key), @@ -54,9 +56,10 @@ impl<'a, T> SnapshotItem<'a, T> { } } -impl<'a, T> SnapshotItem<'a, T> +impl<'a, T, S> SnapshotItem<'a, T, S> where T: Serialize + DeserializeOwned + Clone, + S: SnapshotStrategy<'a, (), T>, { /// load old value and store changelog fn write_change(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { @@ -70,14 +73,14 @@ where } pub fn save(&self, store: &mut dyn Storage, data: &T, height: u64) -> StdResult<()> { - if self.snapshots.should_checkpoint(store, &())? { + if self.snapshots.should_archive(store, &(), height)? { self.write_change(store, height)?; } self.primary.save(store, data) } pub fn remove(&self, store: &mut dyn Storage, height: u64) -> StdResult<()> { - if self.snapshots.should_checkpoint(store, &())? { + if self.snapshots.should_archive(store, &(), height)? { self.write_change(store, height)?; } self.primary.remove(store); diff --git a/src/snapshot/map.rs b/src/snapshot/map.rs index 3d7e6b0..a37a268 100644 --- a/src/snapshot/map.rs +++ b/src/snapshot/map.rs @@ -13,15 +13,17 @@ use crate::prefix::{namespaced_prefix_range, Prefix}; use crate::snapshot::{ChangeSet, Snapshot}; use crate::{Bound, Prefixer, Strategy}; +use super::SnapshotStrategy; + /// Map that maintains a snapshots of one or more checkpoints. /// We can query historical data as well as current state. /// What data is snapshotted depends on the Strategy. -pub struct SnapshotMap<'a, K, T> { +pub struct SnapshotMap<'a, K, T, S = Strategy> { primary: Map<'a, K, T>, - snapshots: Snapshot<'a, K, T>, + snapshots: Snapshot<'a, K, T, S>, } -impl<'a, K, T> SnapshotMap<'a, K, T> { +impl<'a, K, T, S> SnapshotMap<'a, K, T, S> { /// Example: /// /// ```rust @@ -34,12 +36,7 @@ impl<'a, K, T> SnapshotMap<'a, K, T> { /// Strategy::EveryBlock /// ); /// ``` - pub const fn new( - pk: &'a str, - checkpoints: &'a str, - changelog: &'a str, - strategy: Strategy, - ) -> Self { + pub const fn new(pk: &'a str, checkpoints: &'a str, changelog: &'a str, strategy: S) -> Self { SnapshotMap { primary: Map::new(pk), snapshots: Snapshot::new(checkpoints, changelog, strategy), @@ -51,7 +48,7 @@ impl<'a, K, T> SnapshotMap<'a, K, T> { } } -impl<'a, K, T> SnapshotMap<'a, K, T> +impl<'a, K, T, S> SnapshotMap<'a, K, T, S> where T: Serialize + DeserializeOwned + Clone, K: PrimaryKey<'a> + Prefixer<'a>, @@ -65,10 +62,11 @@ where } } -impl<'a, K, T> SnapshotMap<'a, K, T> +impl<'a, K, T, S> SnapshotMap<'a, K, T, S> where T: Serialize + DeserializeOwned + Clone, K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize, + S: SnapshotStrategy<'a, K, T>, { pub fn key(&self, k: K) -> Path { self.primary.key(k) @@ -90,14 +88,14 @@ where } pub fn save(&self, store: &mut dyn Storage, k: K, data: &T, height: u64) -> StdResult<()> { - if self.snapshots.should_checkpoint(store, &k)? { + if self.snapshots.should_archive(store, &k, height)? { self.write_change(store, k.clone(), height)?; } self.primary.save(store, k, data) } pub fn remove(&self, store: &mut dyn Storage, k: K, height: u64) -> StdResult<()> { - if self.snapshots.should_checkpoint(store, &k)? { + if self.snapshots.should_archive(store, &k, height)? { self.write_change(store, k.clone(), height)?; } self.primary.remove(store, k); @@ -162,10 +160,11 @@ where } // short-cut for simple keys, rather than .prefix(()).range_raw(...) -impl<'a, K, T> SnapshotMap<'a, K, T> +impl<'a, K, T, S> SnapshotMap<'a, K, T, S> where T: Serialize + DeserializeOwned + Clone, K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize, + S: SnapshotStrategy<'a, K, T>, { // I would prefer not to copy code from Prefix, but no other way // with lifetimes (create Prefix inside function and return ref = no no) @@ -197,7 +196,7 @@ where } #[cfg(feature = "iterator")] -impl<'a, K, T> SnapshotMap<'a, K, T> +impl<'a, K, T, S> SnapshotMap<'a, K, T, S> where T: Serialize + DeserializeOwned, K: PrimaryKey<'a> + KeyDeserialize, diff --git a/src/snapshot/mod.rs b/src/snapshot/mod.rs index 7fccfea..9c702da 100644 --- a/src/snapshot/mod.rs +++ b/src/snapshot/mod.rs @@ -1,7 +1,9 @@ #![cfg(feature = "iterator")] +mod interval_strategy; mod item; mod map; +pub use interval_strategy::IntervalStrategy; pub use item::SnapshotItem; pub use map::SnapshotMap; @@ -17,23 +19,23 @@ use serde::{Deserialize, Serialize}; /// been checkpointed (as u32). /// Stores all changes in changelog. #[derive(Debug, Clone)] -pub(crate) struct Snapshot<'a, K, T> { +pub(crate) struct Snapshot<'a, K, T, S> { checkpoints: Map<'a, u64, u32>, // this stores all changes (key, height). Must differentiate between no data written, // and explicit None (just inserted) pub changelog: Map<'a, (K, u64), ChangeSet>, - // How aggressive we are about checkpointing all data - strategy: Strategy, + // The strategy for deciding when to archive data + strategy: S, } -impl<'a, K, T> Snapshot<'a, K, T> { +impl<'a, K, T, S> Snapshot<'a, K, T, S> { pub const fn new( checkpoints: &'a str, changelog: &'a str, - strategy: Strategy, - ) -> Snapshot<'a, K, T> { + strategy: S, + ) -> Snapshot<'a, K, T, S> { Snapshot { checkpoints: Map::new(checkpoints), changelog: Map::new(changelog), @@ -61,59 +63,29 @@ impl<'a, K, T> Snapshot<'a, K, T> { } } -impl<'a, K, T> Snapshot<'a, K, T> +impl<'a, K, T, S> Snapshot<'a, K, T, S> where T: Serialize + DeserializeOwned + Clone, K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize, + S: SnapshotStrategy<'a, K, T>, { - /// should_checkpoint looks at the strategy and determines if we want to checkpoint - pub fn should_checkpoint(&self, store: &dyn Storage, k: &K) -> StdResult { - match self.strategy { - Strategy::EveryBlock => Ok(true), - Strategy::Never => Ok(false), - Strategy::Selected => self.should_checkpoint_selected(store, k), - } + pub fn should_archive(&self, store: &dyn Storage, key: &K, height: u64) -> StdResult { + self.strategy + .should_archive(store, &self.checkpoints, &self.changelog, key, height) } - /// this is just pulled out from above for the selected block - fn should_checkpoint_selected(&self, store: &dyn Storage, k: &K) -> StdResult { - // most recent checkpoint - let checkpoint = self - .checkpoints - .range(store, None, None, Order::Descending) - .next() - .transpose()?; - if let Some((height, _)) = checkpoint { - // any changelog for the given key since then? - let start = Bound::inclusive(height); - let first = self - .changelog - .prefix(k.clone()) - .range_raw(store, Some(start), None, Order::Ascending) - .next() - .transpose()?; - if first.is_none() { - // there must be at least one open checkpoint and no changelog for the given height since then - return Ok(true); - } - } - // otherwise, we don't save this - Ok(false) - } - - // If there is no checkpoint for that height, then we return StdError::NotFound pub fn assert_checkpointed(&self, store: &dyn Storage, height: u64) -> StdResult<()> { - let has = match self.strategy { - Strategy::EveryBlock => true, - Strategy::Never => false, - Strategy::Selected => self.checkpoints.may_load(store, height)?.is_some(), - }; - match has { - true => Ok(()), - false => Err(StdError::not_found("checkpoint")), - } + self.strategy + .assert_checkpointed(store, &self.checkpoints, height) } +} +impl<'a, K, T, S> Snapshot<'a, K, T, S> +where + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize, + S: SnapshotStrategy<'a, K, T>, +{ pub fn has_changelog(&self, store: &mut dyn Storage, key: K, height: u64) -> StdResult { Ok(self.changelog.may_load(store, (key, height))?.is_some()) } @@ -160,6 +132,35 @@ where } } +pub trait SnapshotStrategy<'a, K, T> +where + Self: Sized, + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize, +{ + /// Whether or not we have a checkpoint at the given height. This is used in + /// Snapshot::may_load_at_height and is checked before trying to load changelog entries. + /// Therefore, return Ok(()) if you are not using the checkpoint feature to still allow + /// loading the changelog. + fn assert_checkpointed( + &self, + store: &dyn Storage, + checkpoints: &Map<'a, u64, u32>, + height: u64, + ) -> StdResult<()>; + + /// Whether or not we should archive the previously stored data to the changelog for the + /// given key and height. + fn should_archive( + &self, + store: &dyn Storage, + checkpoints: &Map<'a, u64, u32>, + changelog: &Map<'a, (K, u64), ChangeSet>, + key: &K, + height: u64, + ) -> StdResult; +} + #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Strategy { EveryBlock, @@ -172,6 +173,67 @@ pub enum Strategy { Selected, } +impl<'a, K, T> SnapshotStrategy<'a, K, T> for Strategy +where + T: Serialize + DeserializeOwned + Clone, + K: PrimaryKey<'a> + Prefixer<'a> + KeyDeserialize, +{ + // If there is no checkpoint for that height, then we return StdError::NotFound + fn assert_checkpointed( + &self, + store: &dyn Storage, + checkpoints: &Map<'a, u64, u32>, + height: u64, + ) -> StdResult<()> { + let has = match self { + Self::EveryBlock => true, + Self::Never => false, + Self::Selected => checkpoints.may_load(store, height)?.is_some(), + }; + match has { + true => Ok(()), + false => Err(StdError::not_found("checkpoint")), + } + } + + /// should_checkpoint looks at the strategy and determines if we want to checkpoint + fn should_archive( + &self, + store: &dyn Storage, + checkpoints: &Map<'a, u64, u32>, + changelog: &Map<'a, (K, u64), ChangeSet>, + k: &K, + _height: u64, + ) -> StdResult { + match self { + Strategy::EveryBlock => Ok(true), + Strategy::Never => Ok(false), + Strategy::Selected => { + // most recent checkpoint + let checkpoint = checkpoints + .range(store, None, None, Order::Descending) + .next() + .transpose()?; + if let Some((height, _)) = checkpoint { + // any changelog for the given key since then? + let start = Bound::inclusive(height); + let first = changelog + .prefix(k.clone()) + .range_raw(store, Some(start), None, Order::Ascending) + .next() + .transpose()?; + if first.is_none() { + // there must be at least one open checkpoint and no changelog for the given height since then + return Ok(true); + } + } + // otherwise, we don't save this + Ok(false) + } + } + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct ChangeSet { pub old: Option, @@ -182,7 +244,7 @@ mod tests { use super::*; use cosmwasm_std::testing::MockStorage; - type TestSnapshot = Snapshot<'static, &'static str, u64>; + type TestSnapshot = Snapshot<'static, &'static str, u64, Strategy>; const NEVER: TestSnapshot = Snapshot::new("never__check", "never__change", Strategy::Never); const EVERY: TestSnapshot = @@ -196,9 +258,9 @@ mod tests { fn should_checkpoint() { let storage = MockStorage::new(); - assert_eq!(NEVER.should_checkpoint(&storage, &DUMMY_KEY), Ok(false)); - assert_eq!(EVERY.should_checkpoint(&storage, &DUMMY_KEY), Ok(true)); - assert_eq!(SELECT.should_checkpoint(&storage, &DUMMY_KEY), Ok(false)); + assert_eq!(NEVER.should_archive(&storage, &DUMMY_KEY, 0), Ok(false)); + assert_eq!(EVERY.should_archive(&storage, &DUMMY_KEY, 0), Ok(true)); + assert_eq!(SELECT.should_archive(&storage, &DUMMY_KEY, 0), Ok(false)); } #[test]