use core::future::Future;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use freenet_stdlib::prelude::*;
use moka::sync::Cache as MokaCache;
pub const MAX_STATE_SIZE: usize = 50 * 1024 * 1024;
const STATE_HASH_CACHE_CAPACITY: u64 = 100_000;
pub(crate) fn state_hash(state: &WrappedState) -> u64 {
let mut hasher = DefaultHasher::new();
state.as_ref().hash(&mut hasher);
hasher.finish()
}
#[derive(Clone)]
pub(crate) struct StateCacheInvalidator {
state_cache: Option<MokaCache<ContractKey, WrappedState>>,
hash_cache: MokaCache<ContractKey, u64>,
}
impl StateCacheInvalidator {
pub(crate) fn invalidate(&self, key: &ContractKey) {
if let Some(state_cache) = &self.state_cache {
state_cache.invalidate(key);
}
self.hash_cache.invalidate(key);
}
}
#[derive(thiserror::Error, Debug)]
pub enum StateStoreError {
#[error(transparent)]
Any(#[from] anyhow::Error),
#[error("missing contract: {0}")]
MissingContract(ContractKey),
#[error("contract state too large for {key}: {size} bytes exceeds limit of {limit} bytes")]
StateTooLarge {
key: ContractKey,
size: usize,
limit: usize,
},
}
impl From<StateStoreError> for crate::wasm_runtime::ContractError {
fn from(value: StateStoreError) -> Self {
match value {
StateStoreError::Any(err) => {
crate::wasm_runtime::ContractError::from(anyhow::format_err!(err))
}
err @ StateStoreError::MissingContract(_) => {
crate::wasm_runtime::ContractError::from(anyhow::format_err!(err))
}
err @ StateStoreError::StateTooLarge { .. } => {
crate::wasm_runtime::ContractError::from(anyhow::format_err!(err))
}
}
}
}
pub trait StateStorage {
type Error;
fn store(
&self,
key: ContractKey,
state: WrappedState,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn store_params(
&self,
key: ContractKey,
state: Parameters<'static>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn get(
&self,
key: &ContractKey,
) -> impl Future<Output = Result<Option<WrappedState>, Self::Error>> + Send;
fn get_params<'a>(
&'a self,
key: &'a ContractKey,
) -> impl Future<Output = Result<Option<Parameters<'static>>, Self::Error>> + Send + 'a;
fn remove(&self, key: &ContractKey) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[derive(Clone)]
pub struct StateStore<S: StateStorage> {
state_mem_cache: Option<MokaCache<ContractKey, WrappedState>>,
state_hash_cache: MokaCache<ContractKey, u64>,
store: S,
}
impl<S> StateStore<S>
where
S: StateStorage + Send + Sync + 'static,
<S as StateStorage>::Error: Into<anyhow::Error>,
{
pub fn new(store: S, max_size: u32) -> Result<Self, StateStoreError> {
let cache = MokaCache::builder()
.max_capacity(max_size as u64)
.weigher(|key: &ContractKey, value: &WrappedState| -> u32 {
u32::try_from(value.size()).unwrap_or_else(|_| {
tracing::warn!(
contract = %key,
size_bytes = value.size(),
"State size exceeds u32::MAX in cache weigher; saturating. \
This should be impossible — MAX_STATE_SIZE enforcement is broken."
);
u32::MAX
})
})
.build();
Ok(Self {
state_mem_cache: Some(cache),
state_hash_cache: Self::build_state_hash_cache(),
store,
})
}
pub fn new_uncached(store: S) -> Self {
Self {
state_mem_cache: None,
state_hash_cache: Self::build_state_hash_cache(),
store,
}
}
fn build_state_hash_cache() -> MokaCache<ContractKey, u64> {
MokaCache::builder()
.max_capacity(STATE_HASH_CACHE_CAPACITY)
.build()
}
pub(crate) fn cache_invalidator(&self) -> StateCacheInvalidator {
StateCacheInvalidator {
state_cache: self.state_mem_cache.clone(),
hash_cache: self.state_hash_cache.clone(),
}
}
pub fn cached_state_hash(&self, key: &ContractKey) -> Option<u64> {
self.state_hash_cache.get(key)
}
pub fn cache_state_hash(&self, key: ContractKey, hash: u64) {
self.state_hash_cache.insert(key, hash);
}
pub async fn update(
&mut self,
key: &ContractKey,
state: WrappedState,
) -> Result<(), StateStoreError> {
if state.size() > MAX_STATE_SIZE {
tracing::warn!(
contract = %key,
size_bytes = state.size(),
limit_bytes = MAX_STATE_SIZE,
"Rejecting oversized state at storage layer (update)"
);
return Err(StateStoreError::StateTooLarge {
key: *key,
size: state.size(),
limit: MAX_STATE_SIZE,
});
}
let cache_miss = if let Some(cache) = &self.state_mem_cache {
cache.get(key).is_none()
} else {
true
};
if cache_miss {
self.store
.get(key)
.await
.map_err(Into::into)?
.ok_or_else(|| StateStoreError::MissingContract(*key))?;
}
self.store
.store(*key, state.clone())
.await
.map_err(Into::into)?;
self.state_hash_cache.invalidate(key);
if let Some(cache) = &self.state_mem_cache {
cache.insert(*key, state);
}
Ok(())
}
pub async fn store(
&mut self,
key: ContractKey,
state: WrappedState,
params: Parameters<'static>,
) -> Result<(), StateStoreError> {
if state.size() > MAX_STATE_SIZE {
tracing::warn!(
contract = %key,
size_bytes = state.size(),
limit_bytes = MAX_STATE_SIZE,
"Rejecting oversized state at storage layer (store)"
);
return Err(StateStoreError::StateTooLarge {
key,
size: state.size(),
limit: MAX_STATE_SIZE,
});
}
self.store
.store(key, state.clone())
.await
.map_err(Into::into)?;
self.state_hash_cache.invalidate(&key);
if let Some(cache) = &self.state_mem_cache {
cache.invalidate(&key);
}
self.store
.store_params(key, params.clone())
.await
.map_err(Into::into)?;
if let Some(cache) = &self.state_mem_cache {
cache.insert(key, state);
}
Ok(())
}
pub async fn get(&self, key: &ContractKey) -> Result<WrappedState, StateStoreError> {
if let Some(cache) = &self.state_mem_cache {
if let Some(v) = cache.get(key) {
return Ok(v);
}
}
let r = self.store.get(key).await.map_err(Into::into)?;
r.ok_or_else(|| StateStoreError::MissingContract(*key))
}
pub async fn delete(&self, key: &ContractKey) -> Result<(), StateStoreError> {
if let Some(cache) = &self.state_mem_cache {
cache.invalidate(key);
}
self.state_hash_cache.invalidate(key);
self.store.remove(key).await.map_err(Into::into)?;
Ok(())
}
pub async fn get_params<'a>(
&'a self,
key: &'a ContractKey,
) -> Result<Option<Parameters<'static>>, StateStoreError> {
let r = self.store.get_params(key).await.map_err(Into::into)?;
Ok(r)
}
pub async fn ensure_params(
&self,
key: ContractKey,
params: Parameters<'static>,
) -> Result<(), StateStoreError> {
self.store
.store_params(key, params)
.await
.map_err(|e| StateStoreError::Any(e.into()))?;
Ok(())
}
pub fn inner(&self) -> &S {
&self.store
}
pub fn storage(&self) -> S
where
S: Clone,
{
self.store.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wasm_runtime::mock_state_storage::MockStateStorage;
fn make_test_key() -> ContractKey {
let code = ContractCode::from(vec![1, 2, 3, 4]);
let params = Parameters::from(vec![5, 6, 7, 8]);
ContractKey::from_params_and_code(¶ms, &code)
}
fn make_test_key_with_code(code_bytes: &[u8]) -> ContractKey {
let code = ContractCode::from(code_bytes.to_vec());
let params = Parameters::from(vec![5, 6, 7, 8]);
ContractKey::from_params_and_code(¶ms, &code)
}
fn make_test_state(data: &[u8]) -> WrappedState {
WrappedState::new(data.to_vec())
}
#[tokio::test]
async fn test_state_store_basic_operations() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, state.clone(), params.clone())
.await
.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, state);
let retrieved_params = store.get_params(&key).await.unwrap();
assert_eq!(retrieved_params, Some(params));
}
#[tokio::test]
async fn test_state_store_get_nonexistent() {
let mock_storage = MockStateStorage::new();
let store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let result = store.get(&key).await;
assert!(matches!(result, Err(StateStoreError::MissingContract(_))));
}
#[tokio::test]
async fn test_state_store_update_nonexistent() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let result = store.update(&key, state).await;
assert!(matches!(result, Err(StateStoreError::MissingContract(_))));
}
#[tokio::test]
async fn test_state_store_update_existing() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let initial_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, initial_state, params).await.unwrap();
store.update(&key, updated_state.clone()).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, updated_state);
}
#[tokio::test]
async fn test_state_store_storage_failure_on_store() {
let mock_storage = MockStateStorage::new();
mock_storage.fail_next_stores(1);
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
let result = store.store(key, state, params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_params_failure_on_store() {
let mock_storage = MockStateStorage::new();
mock_storage.fail_next_store_params(1);
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
let result = store.store(key, state, params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_failure_on_update() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let initial_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, initial_state, params).await.unwrap();
mock_storage.fail_next_stores(1);
let result = store.update(&key, updated_state).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_failure_on_get() {
let mock_storage = MockStateStorage::new();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
mock_storage.seed_state(key, state);
mock_storage.fail_next_gets(1);
let store = StateStore::new(mock_storage, 10_000).unwrap();
let result = store.get(&key).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_cache_populated_on_store() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, state.clone(), params).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, state);
for _ in 0..5 {
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, state);
}
}
#[tokio::test]
async fn test_state_store_cache_update_coherence() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let initial_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, initial_state, params).await.unwrap();
store.update(&key, updated_state.clone()).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, updated_state);
}
#[tokio::test]
async fn test_state_store_multiple_contracts() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key1 = make_test_key_with_code(&[1]);
let key2 = make_test_key_with_code(&[2]);
let key3 = make_test_key_with_code(&[3]);
let state1 = make_test_state(&[10]);
let state2 = make_test_state(&[20]);
let state3 = make_test_state(&[30]);
let params = Parameters::from(vec![5, 6, 7, 8]);
store
.store(key1, state1.clone(), params.clone())
.await
.unwrap();
store
.store(key2, state2.clone(), params.clone())
.await
.unwrap();
store.store(key3, state3.clone(), params).await.unwrap();
assert_eq!(store.get(&key1).await.unwrap(), state1);
assert_eq!(store.get(&key2).await.unwrap(), state2);
assert_eq!(store.get(&key3).await.unwrap(), state3);
}
#[tokio::test]
async fn test_state_store_isolated_failures() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key1 = make_test_key_with_code(&[1]);
let key2 = make_test_key_with_code(&[2]);
let state1 = make_test_state(&[10]);
let state2 = make_test_state(&[20]);
let params = Parameters::from(vec![5, 6, 7, 8]);
store
.store(key1, state1.clone(), params.clone())
.await
.unwrap();
mock_storage.fail_for_key(key2);
let result = store.store(key2, state2, params).await;
assert!(result.is_err());
assert_eq!(store.get(&key1).await.unwrap(), state1);
}
#[tokio::test]
async fn test_state_store_empty_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let empty_state = make_test_state(&[]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, empty_state.clone(), params).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, empty_state);
assert_eq!(retrieved.size(), 0);
}
#[tokio::test]
async fn test_store_rejects_oversized_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let oversized = make_test_state(&vec![0u8; MAX_STATE_SIZE + 1]);
let params = Parameters::from(vec![1, 2, 3]);
let result = store.store(key, oversized, params).await;
assert!(
matches!(result, Err(StateStoreError::StateTooLarge { .. })),
"Expected StateTooLarge, got: {result:?}"
);
}
#[tokio::test]
async fn test_update_rejects_oversized_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let small = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![1, 2, 3]);
store.store(key, small, params).await.unwrap();
let oversized = make_test_state(&vec![0u8; MAX_STATE_SIZE + 1]);
let result = store.update(&key, oversized).await;
assert!(
matches!(result, Err(StateStoreError::StateTooLarge { .. })),
"Expected StateTooLarge, got: {result:?}"
);
}
#[tokio::test]
async fn test_store_accepts_state_at_limit() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let at_limit = make_test_state(&vec![0u8; MAX_STATE_SIZE]);
let params = Parameters::from(vec![1, 2, 3]);
store
.store(key, at_limit, params)
.await
.expect("State at exactly MAX_STATE_SIZE should be accepted");
}
#[tokio::test]
async fn test_state_store_large_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 2_000_000).unwrap();
let key = make_test_key();
let large_data: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
let large_state = make_test_state(&large_data);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, large_state.clone(), params).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, large_state);
assert_eq!(retrieved.size(), 1_000_000);
}
#[tokio::test]
async fn test_uncached_store_failure_leaves_no_state() {
let mock_storage = MockStateStorage::new();
mock_storage.fail_next_stores(1);
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
let result = store.store(key, state, params).await;
assert!(result.is_err());
let get_result = store.get(&key).await;
assert!(
matches!(get_result, Err(StateStoreError::MissingContract(_))),
"Expected MissingContract after failed store, got {:?}",
get_result
);
}
#[tokio::test]
async fn test_uncached_update_failure_preserves_original_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage.clone());
let key = make_test_key();
let original_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, original_state.clone(), params)
.await
.unwrap();
mock_storage.fail_next_stores(1);
let result = store.update(&key, updated_state).await;
assert!(result.is_err());
let retrieved = store.get(&key).await.unwrap();
assert_eq!(
retrieved, original_state,
"Original state should be preserved after failed update"
);
}
#[tokio::test]
async fn test_uncached_sequential_updates_with_failures() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage.clone());
let key = make_test_key();
let state_v1 = make_test_state(&[1]);
let state_v2 = make_test_state(&[2]);
let state_v3 = make_test_state(&[3]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, state_v1.clone(), params).await.unwrap();
store.update(&key, state_v2.clone()).await.unwrap();
assert_eq!(store.get(&key).await.unwrap(), state_v2);
mock_storage.fail_next_stores(1);
let result = store.update(&key, state_v3).await;
assert!(result.is_err());
let retrieved = store.get(&key).await.unwrap();
assert_eq!(
retrieved, state_v2,
"State should be v2 after v3 update failed"
);
}
#[tokio::test]
async fn test_ensure_params_persists_independently() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, state.clone(), params.clone())
.await
.unwrap();
assert_eq!(store.get_params(&key).await.unwrap(), Some(params.clone()));
store.ensure_params(key, params.clone()).await.unwrap();
assert_eq!(store.get_params(&key).await.unwrap(), Some(params));
}
#[tokio::test]
async fn test_ensure_params_fills_gap_when_params_missing() {
let mock_storage = MockStateStorage::new();
let store = StateStore::new_uncached(mock_storage.clone());
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
mock_storage.seed_state(key, state);
assert_eq!(store.get_params(&key).await.unwrap(), None);
store.ensure_params(key, params.clone()).await.unwrap();
assert_eq!(store.get_params(&key).await.unwrap(), Some(params));
}
#[tokio::test]
async fn test_cached_store_failure_does_not_leave_stale_cache() {
let mock_storage = MockStateStorage::new();
mock_storage.fail_next_stores(1);
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
let result = store.store(key, state, params).await;
assert!(result.is_err());
let get_result = store.get(&key).await;
assert!(
matches!(get_result, Err(StateStoreError::MissingContract(_))),
"Cache should not serve data that was never persisted, got {:?}",
get_result
);
}
#[tokio::test]
async fn test_cached_update_failure_preserves_original_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let original_state = make_test_state(&[1, 2, 3]);
let bad_update = make_test_state(&[9, 9, 9]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, original_state.clone(), params)
.await
.unwrap();
mock_storage.fail_next_stores(1);
let result = store.update(&key, bad_update).await;
assert!(result.is_err());
let retrieved = store.get(&key).await.unwrap();
assert_eq!(
retrieved, original_state,
"Cache should serve last persisted state after failed update"
);
}
#[tokio::test]
async fn test_state_store_delete_removes_state_and_params() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, state.clone(), params.clone())
.await
.unwrap();
assert_eq!(store.get(&key).await.unwrap(), state);
assert_eq!(store.get_params(&key).await.unwrap(), Some(params));
store.delete(&key).await.unwrap();
let get_result = store.get(&key).await;
assert!(
matches!(get_result, Err(StateStoreError::MissingContract(_))),
"Expected MissingContract after delete, got {get_result:?}"
);
assert_eq!(store.get_params(&key).await.unwrap(), None);
}
#[tokio::test]
async fn test_state_store_delete_never_stored_is_idempotent() {
let mock_storage = MockStateStorage::new();
let store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
store
.delete(&key)
.await
.expect("delete of a never-stored contract should be Ok");
}
#[test]
fn state_hash_distinguishes_different_states_and_is_stable() {
let a = make_test_state(&[1, 2, 3]);
let a_again = make_test_state(&[1, 2, 3]);
let b = make_test_state(&[1, 2, 4]);
assert_eq!(
state_hash(&a),
state_hash(&a_again),
"equal state bytes must hash equally"
);
assert_ne!(
state_hash(&a),
state_hash(&b),
"different state bytes must (with overwhelming probability) hash differently"
);
}
#[tokio::test]
async fn state_hash_cache_invalidated_by_every_write_path() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let params = Parameters::from(vec![1, 2, 3]);
let state_a = make_test_state(&[1, 2, 3]);
let state_b = make_test_state(&[4, 5, 6]);
assert_eq!(store.cached_state_hash(&key), None);
store
.store(key, state_a.clone(), params.clone())
.await
.unwrap();
assert_eq!(
store.cached_state_hash(&key),
None,
"store() must leave the detector absent so the next summarize recomputes"
);
let h_a = state_hash(&state_a);
store.cache_state_hash(key, h_a);
assert_eq!(store.cached_state_hash(&key), Some(h_a));
store.update(&key, state_b.clone()).await.unwrap();
assert_eq!(
store.cached_state_hash(&key),
None,
"update() must invalidate the detector"
);
let h_b = state_hash(&state_b);
assert_ne!(h_a, h_b);
store.cache_state_hash(key, h_b);
assert_eq!(store.cached_state_hash(&key), Some(h_b));
store.delete(&key).await.unwrap();
assert_eq!(
store.cached_state_hash(&key),
None,
"delete() must invalidate the detector"
);
}
#[tokio::test]
async fn cache_invalidator_clears_detector_hash() {
let mock_storage = MockStateStorage::new();
let store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
store.cache_state_hash(key, 7);
assert_eq!(store.cached_state_hash(&key), Some(7));
let invalidator = store.cache_invalidator();
invalidator.invalidate(&key);
assert_eq!(
store.cached_state_hash(&key),
None,
"cache_invalidator().invalidate must clear the detector the fast path reads"
);
}
#[tokio::test]
async fn cache_invalidator_drops_stale_state_bytes_from_moka() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let params = Parameters::from(vec![1, 2, 3]);
let state_a = make_test_state(&[1, 2, 3]);
let state_b = make_test_state(&[4, 5, 6, 7]);
store.store(key, state_a.clone(), params).await.unwrap();
assert_eq!(store.get(&key).await.unwrap(), state_a);
store.cache_state_hash(key, state_hash(&state_a));
mock_storage.seed_state(key, state_b.clone());
assert_eq!(
store.get(&key).await.unwrap(),
state_a,
"moka cache masks the bypass write until invalidated (the bug)"
);
store.cache_invalidator().invalidate(&key);
assert_eq!(
store.cached_state_hash(&key),
None,
"detector hash must be cleared"
);
assert_eq!(
store.get(&key).await.unwrap(),
state_b,
"after invalidation, get must reload the fresh bypass-written bytes"
);
}
#[tokio::test]
async fn store_invalidates_caches_even_when_params_write_fails() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let params = Parameters::from(vec![1, 2, 3]);
let state_a = make_test_state(&[1, 2, 3]);
let state_b = make_test_state(&[4, 5, 6, 7]);
store
.store(key, state_a.clone(), params.clone())
.await
.unwrap();
store.cache_state_hash(key, state_hash(&state_a));
assert_eq!(store.cached_state_hash(&key), Some(state_hash(&state_a)));
mock_storage.fail_next_store_params(1);
let res = store.store(key, state_b.clone(), params).await;
assert!(res.is_err(), "store must surface the params write failure");
assert_eq!(
store.cached_state_hash(&key),
None,
"detector must be cleared once the state write commits"
);
assert_eq!(
store.get(&key).await.unwrap(),
state_b,
"get must reload the committed state after a params-write failure"
);
}
#[tokio::test]
async fn state_hash_cache_present_in_cached_mode() {
let mock_storage = MockStateStorage::new();
let store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
assert_eq!(store.cached_state_hash(&key), None);
store.cache_state_hash(key, 42);
assert_eq!(store.cached_state_hash(&key), Some(42));
}
}