-
Notifications
You must be signed in to change notification settings - Fork 251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement caching layer for LazyOption #444
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
5bdeeaa
Initial work on lazy option caching
ChaoticTempest 319cab5
Lazy option tests now work
ChaoticTempest a6281f1
Added replace
ChaoticTempest 06f2543
Add more methods from LazyOption
ChaoticTempest db36b89
Expose LazyOption in mod.rs
ChaoticTempest 1a6199a
Impl Deref/DerefMut for LazyOption
ChaoticTempest 553fda7
Moved LazyOption::storage_remove to flush
ChaoticTempest ce3f320
Remove unneeded parenthesis
ChaoticTempest 73af9c6
Change set to deref_mut in test
ChaoticTempest c1d06b8
Updated docs
ChaoticTempest 9bd2c28
Merge branch 'master' into feature/lazy-option-caching
ChaoticTempest a252c23
Ran cargo format
ChaoticTempest b428a98
Moved LazyOption from mod.rs into lazy_option.rs
ChaoticTempest 3e1eced
Getting rid of extra API
ChaoticTempest eacfbd1
Updated docs
ChaoticTempest 237c23e
Cargo format
ChaoticTempest bd91b78
Change Vec<u8> into Box<[u8]>
ChaoticTempest 3c49a7a
Moved LazyOption into separate folder
ChaoticTempest f8f975c
Add to changelog
ChaoticTempest 61999b7
Merge branch 'master' into feature/lazy-option-caching
ChaoticTempest File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
use borsh::{BorshDeserialize, BorshSerialize}; | ||
|
||
use super::LazyOption; | ||
|
||
impl<T> Drop for LazyOption<T> | ||
where | ||
T: BorshSerialize, | ||
{ | ||
fn drop(&mut self) { | ||
self.flush() | ||
} | ||
} | ||
|
||
impl<T> core::ops::Deref for LazyOption<T> | ||
where | ||
T: BorshSerialize + BorshDeserialize, | ||
{ | ||
type Target = Option<T>; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
Self::get(self) | ||
} | ||
} | ||
|
||
impl<T> core::ops::DerefMut for LazyOption<T> | ||
where | ||
T: BorshSerialize + BorshDeserialize, | ||
{ | ||
fn deref_mut(&mut self) -> &mut Self::Target { | ||
Self::get_mut(self) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
mod impls; | ||
|
||
use borsh::{BorshDeserialize, BorshSerialize}; | ||
use once_cell::unsync::OnceCell; | ||
|
||
use crate::env; | ||
use crate::store::lazy::{load_and_deserialize, serialize_and_store}; | ||
use crate::utils::{CacheEntry, EntryState}; | ||
use crate::IntoStorageKey; | ||
|
||
/// An persistent lazily loaded option, that stores a `value` in the storage when `Some(value)` | ||
/// is set, and not when `None` is set. `LazyOption` also [`Deref`]s into [`Option`] so we get | ||
/// all its APIs for free. | ||
/// | ||
/// This will only write to the underlying store if the value has changed, and will only read the | ||
/// existing value from storage once. | ||
/// | ||
/// # Examples | ||
/// ``` | ||
/// use near_sdk::store::LazyOption; | ||
/// | ||
///# near_sdk::test_utils::test_env::setup(); | ||
/// let mut a = LazyOption::new(b"a", None); | ||
/// assert!(a.is_none()); | ||
/// | ||
/// *a = Some("new value".to_owned()); | ||
/// assert_eq!(a.get(), &Some("new value".to_owned())); | ||
/// | ||
/// // Using Option::replace: | ||
/// let old_str = a.replace("new new value".to_owned()); | ||
/// assert_eq!(old_str, Some("new value".to_owned())); | ||
/// assert_eq!(a.get(), &Some("new new value".to_owned())); | ||
/// ``` | ||
#[derive(BorshSerialize, BorshDeserialize)] | ||
pub struct LazyOption<T> | ||
where | ||
T: BorshSerialize, | ||
{ | ||
/// Key bytes to index the contract's storage. | ||
storage_key: Box<[u8]>, | ||
|
||
/// Cached value which is lazily loaded and deserialized from storage. | ||
#[borsh_skip] | ||
cache: OnceCell<CacheEntry<T>>, | ||
} | ||
|
||
impl<T> LazyOption<T> | ||
where | ||
T: BorshSerialize, | ||
{ | ||
/// Create a new lazy option with the given `storage_key` and the initial value. | ||
pub fn new<S>(storage_key: S, value: Option<T>) -> Self | ||
where | ||
S: IntoStorageKey, | ||
{ | ||
let cache = match value { | ||
Some(value) => CacheEntry::new_modified(Some(value)), | ||
None => CacheEntry::new_cached(None), | ||
}; | ||
|
||
Self { | ||
storage_key: storage_key.into_storage_key().into_boxed_slice(), | ||
cache: OnceCell::from(cache), | ||
} | ||
} | ||
|
||
/// Updates the value with a new value. This does not load the current value from storage. | ||
pub fn set(&mut self, value: Option<T>) { | ||
if let Some(v) = self.cache.get_mut() { | ||
*v.value_mut() = value; | ||
} else { | ||
self.cache | ||
.set(CacheEntry::new_modified(value)) | ||
.ok() | ||
.expect("cache is checked to not be filled above"); | ||
} | ||
} | ||
|
||
/// Writes any changes to the value to storage. This will automatically be done when the | ||
/// value is dropped through [`Drop`] so this should only be used when the changes need to be | ||
/// reflected in the underlying storage before then. | ||
pub fn flush(&mut self) { | ||
if let Some(v) = self.cache.get_mut() { | ||
if !v.is_modified() { | ||
return; | ||
} | ||
|
||
match v.value().as_ref() { | ||
Some(value) => serialize_and_store(&self.storage_key, value), | ||
None => { | ||
env::storage_remove(&self.storage_key); | ||
} | ||
} | ||
|
||
// Replaces cache entry state to cached because the value in memory matches the | ||
// stored value. This avoids writing the same value twice. | ||
v.replace_state(EntryState::Cached); | ||
} | ||
} | ||
} | ||
|
||
impl<T> LazyOption<T> | ||
where | ||
T: BorshSerialize + BorshDeserialize, | ||
{ | ||
/// Returns a reference to the lazily loaded optional. | ||
/// The load from storage only happens once, and if the value is already cached, it will not | ||
/// be reloaded. | ||
pub fn get(&self) -> &Option<T> { | ||
let entry = self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)); | ||
entry.value() | ||
} | ||
|
||
/// Returns a reference to the lazily loaded optional. | ||
/// The load from storage only happens once, and if the value is already cached, it will not | ||
/// be reloaded. | ||
pub fn get_mut(&mut self) -> &mut Option<T> { | ||
self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)); | ||
let entry = self.cache.get_mut().expect("cell should be filled above"); | ||
entry.value_mut() | ||
} | ||
} | ||
|
||
#[cfg(not(target_arch = "wasm32"))] | ||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::test_utils::test_env; | ||
|
||
#[test] | ||
pub fn test_lazy_option() { | ||
test_env::setup(); | ||
let mut a = LazyOption::new(b"a", None); | ||
assert!(a.is_none()); | ||
assert!(!env::storage_has_key(b"a")); | ||
|
||
// Check value has been set in via cache: | ||
a.set(Some(42u32)); | ||
assert!(a.is_some()); | ||
assert_eq!(a.get(), &Some(42)); | ||
|
||
// Flushing, then check if storage has been set: | ||
a.flush(); | ||
assert!(env::storage_has_key(b"a")); | ||
assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 42); | ||
|
||
// New value is set | ||
*a = Some(49u32); | ||
assert!(a.is_some()); | ||
assert_eq!(a.get(), &Some(49)); | ||
|
||
// Testing `Option::replace` | ||
let old = a.replace(69u32); | ||
assert!(a.is_some()); | ||
assert_eq!(old, Some(49)); | ||
|
||
// Testing `Option::take` deletes from internal storage | ||
let taken = a.take(); | ||
assert!(a.is_none()); | ||
assert_eq!(taken, Some(69)); | ||
|
||
// `flush`/`drop` after `Option::take` should remove from storage: | ||
drop(a); | ||
assert!(!env::storage_has_key(b"a")); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
mod lazy; | ||
mod lazy_option; | ||
pub use lazy::Lazy; | ||
pub use lazy_option::LazyOption; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice