1pub mod coherence;
37#[cfg(feature = "maud")]
38mod fragment;
39mod layer;
40#[cfg(feature = "cache-moka")]
41mod moka_impl;
42mod read_through;
43
44#[cfg(feature = "maud")]
45pub use fragment::{
46 cache_fragment, cache_fragment_global, cache_fragment_global_in, cache_fragment_in,
47};
48pub use layer::{CacheResponseLayer, CacheResponseService};
49#[cfg(feature = "cache-moka")]
50pub use moka_impl::MokaCache;
51pub use read_through::{
52 CacheFillError, GetOrComputeOptions, ReadThroughMetrics, ReadThroughMetricsSnapshot,
53 get_or_compute, get_or_compute_with, jittered_ttl, read_through_metrics,
54};
55
56use std::any::Any;
57use std::hash::{DefaultHasher, Hash, Hasher};
58use std::sync::{Arc, RwLock};
59use std::time::Duration;
60
61static GLOBAL_CACHE: RwLock<Option<Arc<dyn Cache>>> = RwLock::new(None);
68
69pub fn set_global_cache(cache: Arc<dyn Cache>) {
84 *GLOBAL_CACHE.write().expect("global cache lock poisoned") = Some(with_capsule_seam(cache));
85}
86
87#[must_use]
96pub fn global_cache() -> Option<Arc<dyn Cache>> {
97 GLOBAL_CACHE
98 .read()
99 .expect("global cache lock poisoned")
100 .clone()
101}
102
103#[cfg(feature = "reporting")]
115pub(crate) fn install_replay_cache(builder_cache: bool) -> Option<Arc<dyn Cache>> {
116 if !builder_cache {
117 clear_global_cache();
118 return None;
119 }
120 let cache = with_capsule_seam(Arc::new(ReplayBackend));
122 set_global_cache(Arc::clone(&cache));
123 Some(cache)
124}
125
126#[cfg(feature = "reporting")]
131pub(crate) fn install_late_replay_cache(
132 state: &crate::state::AppState,
133 recorded: &crate::capsule::CapsuleEffects,
134) {
135 if recorded.state_cache && state.cache().is_none() {
136 state.set_cache(Arc::new(ReplayBackend));
137 }
138}
139
140#[cfg(feature = "reporting")]
146pub(crate) fn replayed_namespace_removal(namespace: &str) -> Option<bool> {
147 if let Some(tape) = crate::capsule::effects::current_tape() {
148 return Some(tape.cache_invalidate_namespace(namespace).is_ok());
149 }
150 replay_blocked().then_some(true)
151}
152
153#[cfg(not(feature = "reporting"))]
155pub(crate) const fn replayed_namespace_removal(_namespace: &str) -> Option<bool> {
156 None
157}
158
159#[cfg(feature = "reporting")]
167pub(crate) struct NamespaceRemoval(Option<(Arc<crate::capsule::CaptureScope>, usize)>);
168
169#[cfg(feature = "reporting")]
170impl NamespaceRemoval {
171 pub(crate) fn reserve() -> Self {
173 Self(
174 crate::capsule::current_scope()
175 .and_then(|scope| scope.reserve_cache().map(|index| (scope, index))),
176 )
177 }
178
179 pub(crate) fn call_installed<R>(call: impl FnOnce() -> R) -> R {
182 let outer = COHERENCE_REMOVAL.with(|flag| flag.replace(true));
183 let restore = RestoreFlag(outer);
184 let result = call();
185 drop(restore);
186 result
187 }
188
189 pub(crate) fn record(self, namespace: &str, failure: Option<&str>) {
191 if let Some((scope, index)) = self.0 {
192 scope.fill_cache(
193 index,
194 crate::capsule::CacheEffect::InvalidateNamespace {
195 namespace: namespace.to_owned(),
196 error: failure.map(|reason| crate::capsule::CacheInvalidationError {
197 attempts: 1,
198 reason: reason.to_owned(),
199 }),
200 },
201 );
202 }
203 }
204}
205
206#[cfg(feature = "reporting")]
208thread_local! {
209 static COHERENCE_REMOVAL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
210}
211
212#[cfg(feature = "reporting")]
214struct RestoreFlag(bool);
215
216#[cfg(feature = "reporting")]
217impl Drop for RestoreFlag {
218 fn drop(&mut self) {
219 let outer = self.0;
220 COHERENCE_REMOVAL.with(|flag| flag.set(outer));
221 }
222}
223
224#[cfg(not(feature = "reporting"))]
226pub(crate) struct NamespaceRemoval;
227
228#[cfg(not(feature = "reporting"))]
229impl NamespaceRemoval {
230 pub(crate) const fn reserve() -> Self {
231 Self
232 }
233
234 pub(crate) fn call_installed<R>(call: impl FnOnce() -> R) -> R {
235 call()
236 }
237
238 pub(crate) const fn record(self, _namespace: &str, _failure: Option<&str>) {}
239}
240
241#[cfg(feature = "reporting")]
243struct ReplayBackend;
244
245#[cfg(feature = "reporting")]
246impl Cache for ReplayBackend {
247 fn get_value(&self, _key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
248 None
249 }
250 fn insert_value(&self, _key: &str, _value: Arc<dyn Any + Send + Sync>) {}
251 fn invalidate(&self, _key: &str) {}
252 fn clear(&self) {}
253 fn invalidate_namespace(&self, _namespace: &str) -> bool {
254 true
255 }
256}
257
258pub fn clear_global_cache() {
266 *GLOBAL_CACHE.write().expect("global cache lock poisoned") = None;
267}
268
269pub type CacheFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
273
274#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
279#[error("cache invalidation failed after {attempts} attempt(s): {reason}")]
280pub struct InvalidationError {
281 attempts: u32,
282 reason: String,
283}
284
285impl InvalidationError {
286 pub fn new(attempts: u32, reason: impl Into<String>) -> Self {
288 Self {
289 attempts,
290 reason: reason.into(),
291 }
292 }
293
294 #[must_use]
296 pub const fn attempts(&self) -> u32 {
297 self.attempts
298 }
299
300 #[must_use]
302 pub fn reason(&self) -> &str {
303 &self.reason
304 }
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
312#[error("cache backend failed: {reason}")]
313pub struct CacheBackendError {
314 reason: String,
315}
316
317impl CacheBackendError {
318 pub fn new(reason: impl Into<String>) -> Self {
320 Self {
321 reason: reason.into(),
322 }
323 }
324
325 #[must_use]
327 pub fn reason(&self) -> &str {
328 &self.reason
329 }
330}
331
332static INVALIDATION_FAILURES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
334
335pub fn record_invalidation_failure() {
342 INVALIDATION_FAILURES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
343}
344
345#[must_use]
347pub fn invalidation_failures_total() -> u64 {
348 INVALIDATION_FAILURES.load(std::sync::atomic::Ordering::Relaxed)
349}
350
351pub(crate) static GLOBAL_CACHE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
365
366#[derive(Clone)]
375pub struct RawCacheBytes(pub Vec<u8>);
376
377pub trait Cache: Send + Sync + 'static {
390 fn get_value(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>>;
396
397 fn insert_value(&self, key: &str, value: Arc<dyn Any + Send + Sync>);
399
400 fn invalidate(&self, key: &str);
406
407 fn clear(&self);
409
410 fn invalidate_namespace(&self, _namespace: &str) -> bool {
421 false
422 }
423
424 fn insert_raw_bytes(&self, _key: &str, _bytes: Vec<u8>, _ttl: Option<std::time::Duration>) {}
434
435 fn shares_fill_epoch(&self) -> bool {
442 false
443 }
444
445 fn fill_epoch(&self, _namespace: &str) -> FillEpoch {
458 FillEpoch::Unsupported
459 }
460
461 fn insert_raw_bytes_if_epoch(
471 &self,
472 key: &str,
473 bytes: Vec<u8>,
474 ttl: Option<std::time::Duration>,
475 _namespace: &str,
476 _sampled: u64,
477 ) -> bool {
478 self.insert_raw_bytes(key, bytes, ttl);
479 true
480 }
481
482 fn try_acquire_fill_lock(&self, _key: &str, _token: &str, _ttl: Duration) -> FillLockStatus {
497 FillLockStatus::Unsupported
498 }
499
500 fn release_fill_lock(&self, _key: &str, _token: &str) {}
506
507 fn invalidate_async<'a>(
518 &'a self,
519 key: &'a str,
520 ) -> CacheFuture<'a, Result<(), InvalidationError>> {
521 Box::pin(async move {
522 self.invalidate(key);
523 Ok(())
524 })
525 }
526
527 fn try_get_value(
537 &self,
538 key: &str,
539 ) -> Result<Option<Arc<dyn Any + Send + Sync>>, CacheBackendError> {
540 Ok(self.get_value(key))
541 }
542
543 fn try_insert_raw_bytes(
552 &self,
553 key: &str,
554 bytes: Vec<u8>,
555 ttl: Option<std::time::Duration>,
556 ) -> Result<(), CacheBackendError> {
557 self.insert_raw_bytes(key, bytes, ttl);
558 Ok(())
559 }
560
561 fn try_invalidate(&self, key: &str) -> Result<(), InvalidationError> {
575 self.invalidate(key);
576 Ok(())
577 }
578
579 #[doc(hidden)]
583 fn is_capsule_seam(&self) -> bool {
584 false
585 }
586
587 fn invalidate_namespace_async<'a>(
596 &'a self,
597 namespace: &'a str,
598 ) -> CacheFuture<'a, Result<(), InvalidationError>> {
599 Box::pin(async move {
600 if self.invalidate_namespace(namespace) {
601 Ok(())
602 } else {
603 Err(InvalidationError::new(
604 1,
605 "the backend cannot drop a namespace, or its sweep failed",
606 ))
607 }
608 })
609 }
610}
611
612#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614pub enum FillEpoch {
615 Unsupported,
617 Sampled(u64),
619 Unavailable,
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq)]
626pub enum FillLockStatus {
627 Acquired,
631 Held,
633 Unsupported,
636}
637
638pub fn get<V: Clone + Send + Sync + 'static>(cache: &dyn Cache, key: &str) -> Option<V> {
648 if replayed_untyped_get(key) {
652 return None;
653 }
654 let value = helper_read(cache, key).and_then(|arc| arc.downcast_ref::<V>().cloned());
655 record_untyped_get(key, value.is_some());
656 value
657}
658
659fn helper_read(cache: &dyn Cache, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
662 #[cfg(feature = "reporting")]
663 let _helper = HelperCall::enter();
664 cache.get_value(key)
665}
666
667#[cfg(feature = "reporting")]
670thread_local! {
671 static HELPER_CALL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
672}
673
674#[cfg(feature = "reporting")]
676struct HelperCall(bool);
677
678#[cfg(feature = "reporting")]
679impl HelperCall {
680 fn enter() -> Self {
681 Self(HELPER_CALL.with(|flag| flag.replace(true)))
682 }
683}
684
685#[cfg(feature = "reporting")]
686impl Drop for HelperCall {
687 fn drop(&mut self) {
688 let outer = self.0;
689 HELPER_CALL.with(|flag| flag.set(outer));
690 }
691}
692
693#[cfg(feature = "reporting")]
695const DIRECT_WRITE_NOTE: &str = "the run wrote the cache with `Cache::insert_value` or \
696 `insert_raw_bytes` directly; the write is not recorded, so replay cannot check it";
697
698#[cfg(feature = "reporting")]
700const DIRECT_READ_NOTE: &str = "the run read the cache with `Cache::get_value` directly; the \
701 value is not recorded, so replay cannot serve it";
702
703pub fn insert<V: Clone + Send + Sync + 'static>(cache: &dyn Cache, key: &str, value: V) {
710 if record_or_replay_untyped_insert(key) {
713 return;
714 }
715 #[cfg(feature = "reporting")]
716 let _helper = HelperCall::enter();
717 cache.insert_value(key, Arc::new(value));
718}
719
720pub fn get_cached<V>(cache: &dyn Cache, key: &str) -> Option<V>
728where
729 V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
730{
731 match replayed_cache_get::<V>(key) {
735 ReplayedRead::NoTape => {}
736 ReplayedRead::Miss => return None,
737 ReplayedRead::Hit(value) => return Some(value),
738 }
739 let arc = helper_read(cache, key);
740 let value = arc.and_then(|arc| {
741 if let Some(value) = arc.downcast_ref::<V>() {
743 return Some(value.clone());
744 }
745 arc.downcast_ref::<RawCacheBytes>()
747 .and_then(|raw| serde_json::from_slice::<V>(&raw.0).ok())
748 });
749 record_cache_get(key, value.as_ref());
757 value
758}
759
760pub fn insert_cached<V>(cache: &dyn Cache, key: &str, value: V, ttl: Option<std::time::Duration>)
772where
773 V: Clone + serde::Serialize + Send + Sync + 'static,
774{
775 let bytes = serde_json::to_vec(&value).ok();
776 if record_or_replay_cache_insert(key, bytes.as_deref(), ttl) {
780 return;
781 }
782 #[cfg(feature = "reporting")]
783 let _helper = HelperCall::enter();
784 cache.insert_value(key, Arc::new(value));
786 if let Some(bytes) = bytes {
788 cache.insert_raw_bytes(key, bytes, ttl);
789 }
790}
791
792pub fn try_get_cached<V>(cache: &dyn Cache, key: &str) -> Result<Option<V>, CacheBackendError>
803where
804 V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
805{
806 match replayed_cache_get::<V>(key) {
807 ReplayedRead::NoTape => {}
808 ReplayedRead::Miss => return Ok(None),
809 ReplayedRead::Hit(value) => return Ok(Some(value)),
810 }
811 let read = {
812 #[cfg(feature = "reporting")]
813 let _helper = HelperCall::enter();
814 cache.try_get_value(key)
815 };
816 let found = match read {
817 Ok(found) => found,
818 Err(error) => {
819 mark_failed_cache_op(FAILED_READ_NOTE);
820 return Err(error);
821 }
822 };
823 let Some(arc) = found else {
824 record_cache_get::<V>(key, None);
825 return Ok(None);
826 };
827 if let Some(value) = arc.downcast_ref::<V>() {
828 let value = Some(value.clone());
829 record_cache_get(key, value.as_ref());
830 return Ok(value);
831 }
832 let value = if let Some(raw) = arc.downcast_ref::<RawCacheBytes>() {
835 let Ok(value) = serde_json::from_slice::<V>(&raw.0) else {
836 mark_failed_cache_op(FAILED_READ_NOTE);
837 return Err(CacheBackendError::new("the stored value cannot be decoded"));
838 };
839 Some(value)
840 } else {
841 None
842 };
843 record_cache_get(key, value.as_ref());
844 Ok(value)
845}
846
847pub fn try_insert_cached<V>(
854 cache: &dyn Cache,
855 key: &str,
856 value: V,
857 ttl: Option<std::time::Duration>,
858) -> Result<(), CacheBackendError>
859where
860 V: Clone + serde::Serialize + Send + Sync + 'static,
861{
862 let bytes = serde_json::to_vec(&value).map_err(|error| {
863 CacheBackendError::new(format!("the value cannot be serialized: {error}"))
864 })?;
865 if record_or_replay_cache_insert(key, Some(&bytes), ttl) {
866 return Ok(());
867 }
868 #[cfg(feature = "reporting")]
869 let _helper = HelperCall::enter();
870 cache.insert_value(key, Arc::new(value));
871 let stored = cache.try_insert_raw_bytes(key, bytes, ttl);
872 if stored.is_err() {
873 mark_failed_cache_op(FAILED_WRITE_NOTE);
875 }
876 stored
877}
878
879const FAILED_READ_NOTE: &str = "a cache read failed; the failure is not recorded, so replay \
881 cannot take the same path";
882
883const FAILED_WRITE_NOTE: &str = "a cache write failed; the capsule recorded it as stored, so \
885 replay cannot take the same path";
886
887#[cfg(feature = "reporting")]
889fn mark_failed_cache_op(note: &str) {
890 if let Some(scope) = crate::capsule::current_scope() {
891 scope.note(note);
892 scope.mark_truncated();
893 }
894}
895
896#[cfg(not(feature = "reporting"))]
898const fn mark_failed_cache_op(_note: &str) {}
899
900pub fn insert_cached_fenced<V>(
908 cache: &dyn Cache,
909 key: &str,
910 value: V,
911 ttl: Option<std::time::Duration>,
912 namespace: &str,
913 epoch: FillEpoch,
914) -> bool
915where
916 V: Clone + serde::Serialize + Send + Sync + 'static,
917{
918 let epoch = match epoch {
923 FillEpoch::Sampled(_) if replaying() => {
924 if serde_json::to_vec(&value).is_err() {
925 return false;
926 }
927 FillEpoch::Unsupported
928 }
929 other => other,
930 };
931 let sampled = match epoch {
932 FillEpoch::Unsupported => {
933 insert_cached(cache, key, value, ttl);
934 return true;
935 }
936 FillEpoch::Unavailable => return false,
937 FillEpoch::Sampled(sampled) => sampled,
938 };
939 let Some(bytes) = serde_json::to_vec(&value).ok() else {
940 return false;
941 };
942 let stored = {
944 #[cfg(feature = "reporting")]
945 let _helper = HelperCall::enter();
946 cache.insert_raw_bytes_if_epoch(key, bytes.clone(), ttl, namespace, sampled)
947 };
948 if stored {
949 let _ = record_or_replay_cache_insert(key, Some(&bytes), ttl);
952 }
953 stored
954}
955
956#[must_use]
963pub fn sample_fill_epoch(cache: &dyn Cache, namespace: &str) -> FillEpoch {
964 if replaying() {
965 return FillEpoch::Unsupported;
966 }
967 cache.fill_epoch(namespace)
968}
969
970#[cfg(feature = "reporting")]
976fn replaying() -> bool {
977 crate::capsule::effects::current_tape().is_some() || replay_blocked()
978}
979
980#[cfg(not(feature = "reporting"))]
982const fn replaying() -> bool {
983 false
984}
985
986enum ReplayedRead<V> {
998 NoTape,
1000 Miss,
1004 Hit(V),
1006}
1007
1008#[cfg(feature = "reporting")]
1010fn replayed_cache_get<V>(key: &str) -> ReplayedRead<V>
1011where
1012 V: Clone + serde::de::DeserializeOwned + Send + Sync + 'static,
1013{
1014 use crate::capsule::effects::CachedValue;
1015 let Some(tape) = crate::capsule::effects::current_tape() else {
1016 if replay_blocked() {
1019 return ReplayedRead::Miss;
1020 }
1021 return ReplayedRead::NoTape;
1022 };
1023 match tape.cache_get(key) {
1024 CachedValue::Hit(bytes) => {
1025 serde_json::from_slice::<V>(&bytes).map_or(ReplayedRead::Miss, ReplayedRead::Hit)
1026 }
1027 CachedValue::Miss | CachedValue::Unrecorded => ReplayedRead::Miss,
1028 }
1029}
1030
1031#[cfg(not(feature = "reporting"))]
1033const fn replayed_cache_get<V>(_key: &str) -> ReplayedRead<V>
1034where
1035 V: Clone + serde::de::DeserializeOwned + Send + Sync + 'static,
1036{
1037 ReplayedRead::NoTape
1038}
1039
1040#[cfg(feature = "reporting")]
1053fn record_cache_get<V: serde::Serialize>(key: &str, value: Option<&V>) {
1054 let Some(scope) = crate::capsule::current_scope() else {
1055 return;
1056 };
1057 let encoded = value.map(|value| {
1058 serde_json::to_vec(value).map(|bytes| {
1059 use base64::Engine as _;
1060 base64::engine::general_purpose::STANDARD.encode(&bytes)
1061 })
1062 });
1063 let encoded = match encoded {
1064 None => None,
1065 Some(Ok(encoded)) => Some(encoded),
1066 Some(Err(_)) => {
1067 scope.note(UNRECORDABLE_CACHE_HIT_NOTE);
1068 scope.mark_truncated();
1069 None
1070 }
1071 };
1072 scope.record_cache(crate::capsule::CacheEffect::Get {
1073 key: key.to_owned(),
1074 value: encoded,
1075 });
1076}
1077
1078#[cfg(feature = "reporting")]
1080const UNRECORDABLE_CACHE_WRITE_NOTE: &str = "a cache write held a value that could not be serialized into the capsule, so replay would \
1081 suppress a mutation the recorded run really made";
1082
1083#[cfg(feature = "reporting")]
1085const UNRECORDABLE_CACHE_HIT_NOTE: &str = "a cache read hit a value that could not be serialized into the capsule, and a hit with no \
1086 recorded value is indistinguishable from a miss; replay would take the miss branch the \
1087 recorded run never took";
1088
1089#[cfg(not(feature = "reporting"))]
1091const fn record_cache_get<V: serde::Serialize>(_key: &str, _value: Option<&V>) {}
1092
1093#[cfg(feature = "reporting")]
1098fn record_or_replay_cache_insert(
1099 key: &str,
1100 bytes: Option<&[u8]>,
1101 ttl: Option<std::time::Duration>,
1102) -> bool {
1103 if let Some(tape) = crate::capsule::effects::current_tape() {
1104 if let Some(bytes) = bytes {
1105 tape.cache_insert(key, bytes, ttl.map(|ttl| ttl.as_secs()));
1106 }
1107 return true;
1108 }
1109 if replay_blocked() {
1112 return true;
1113 }
1114 if let Some(scope) = crate::capsule::current_scope() {
1115 if let Some(bytes) = bytes {
1116 use base64::Engine as _;
1117 scope.record_cache(crate::capsule::CacheEffect::Insert {
1118 key: key.to_owned(),
1119 value: base64::engine::general_purpose::STANDARD.encode(bytes),
1120 ttl_secs: ttl.map(|ttl| ttl.as_secs()),
1121 });
1122 } else {
1123 scope.note(UNRECORDABLE_CACHE_WRITE_NOTE);
1128 scope.mark_truncated();
1129 }
1130 }
1131 false
1132}
1133
1134#[cfg(not(feature = "reporting"))]
1136const fn record_or_replay_cache_insert(
1137 _key: &str,
1138 _bytes: Option<&[u8]>,
1139 _ttl: Option<std::time::Duration>,
1140) -> bool {
1141 false
1142}
1143
1144#[cfg(feature = "reporting")]
1147fn replayed_untyped_get(key: &str) -> bool {
1148 let Some(tape) = crate::capsule::effects::current_tape() else {
1149 return replay_blocked();
1151 };
1152 tape.cache_untyped_get(key);
1153 true
1154}
1155
1156#[cfg(not(feature = "reporting"))]
1158const fn replayed_untyped_get(_key: &str) -> bool {
1159 false
1160}
1161
1162#[cfg(feature = "reporting")]
1165fn record_untyped_get(key: &str, hit: bool) {
1166 let Some(scope) = crate::capsule::current_scope() else {
1167 return;
1168 };
1169 if hit {
1170 scope.note(UNRECORDABLE_CACHE_HIT_NOTE);
1171 scope.mark_truncated();
1172 }
1173 scope.record_cache(crate::capsule::CacheEffect::Get {
1174 key: key.to_owned(),
1175 value: None,
1176 });
1177}
1178
1179#[cfg(not(feature = "reporting"))]
1181const fn record_untyped_get(_key: &str, _hit: bool) {}
1182
1183#[cfg(feature = "reporting")]
1186fn record_or_replay_untyped_insert(key: &str) -> bool {
1187 if let Some(tape) = crate::capsule::effects::current_tape() {
1188 tape.cache_untyped_insert(key);
1189 return true;
1190 }
1191 if replay_blocked() {
1193 return true;
1194 }
1195 if let Some(scope) = crate::capsule::current_scope() {
1196 scope.note(UNRECORDABLE_CACHE_WRITE_NOTE);
1197 scope.mark_truncated();
1198 }
1199 false
1200}
1201
1202#[cfg(not(feature = "reporting"))]
1204const fn record_or_replay_untyped_insert(_key: &str) -> bool {
1205 false
1206}
1207
1208#[cfg(feature = "reporting")]
1214#[must_use]
1215pub(crate) fn with_capsule_seam(cache: Arc<dyn Cache>) -> Arc<dyn Cache> {
1216 if cache.is_capsule_seam() {
1217 return cache;
1218 }
1219 Arc::new(CapsuleSeamCache(cache))
1220}
1221
1222#[cfg(not(feature = "reporting"))]
1224#[must_use]
1225pub(crate) fn with_capsule_seam(cache: Arc<dyn Cache>) -> Arc<dyn Cache> {
1226 cache
1227}
1228
1229#[cfg(feature = "reporting")]
1234struct CapsuleSeamCache(Arc<dyn Cache>);
1235
1236#[cfg(feature = "reporting")]
1237impl CapsuleSeamCache {
1238 fn direct_write_blocked(key: &str) -> bool {
1243 let direct = !HELPER_CALL.with(std::cell::Cell::get);
1244 if let Some(tape) = crate::capsule::effects::current_tape() {
1245 if direct {
1246 tape.cache_untyped_insert(key);
1247 }
1248 return true;
1249 }
1250 if replay_blocked() {
1251 return true;
1252 }
1253 if direct && let Some(scope) = crate::capsule::current_scope() {
1254 scope.note(DIRECT_WRITE_NOTE);
1255 scope.mark_truncated();
1256 }
1257 false
1258 }
1259
1260 fn fence_stopped_fill() {
1262 if let Some(scope) = crate::capsule::current_scope() {
1263 scope.note(FILL_FENCE_NOTE);
1264 scope.mark_truncated();
1265 }
1266 }
1267
1268 fn reserve() -> Option<(Arc<crate::capsule::CaptureScope>, usize)> {
1270 crate::capsule::current_scope()
1271 .and_then(|scope| scope.reserve_cache().map(|index| (scope, index)))
1272 }
1273
1274 fn fill(
1276 slot: Option<(Arc<crate::capsule::CaptureScope>, usize)>,
1277 effect: crate::capsule::CacheEffect,
1278 ) {
1279 if let Some((scope, index)) = slot {
1280 scope.fill_cache(index, effect);
1281 }
1282 }
1283
1284 fn unchecked_removal(remove: impl FnOnce()) {
1289 let before = invalidation_failures_total();
1290 remove();
1291 if invalidation_failures_total() != before
1292 && let Some(scope) = crate::capsule::current_scope()
1293 {
1294 scope.note(UNCHECKED_REMOVAL_NOTE);
1295 scope.mark_truncated();
1296 }
1297 }
1298}
1299
1300#[cfg(feature = "reporting")]
1304static REPLAY_BLOCKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1305
1306#[cfg(feature = "reporting")]
1309pub(crate) fn block_backends_for_replay() {
1310 REPLAY_BLOCKED.store(true, std::sync::atomic::Ordering::SeqCst);
1311}
1312
1313#[cfg(feature = "reporting")]
1314fn replay_blocked() -> bool {
1315 #[cfg(test)]
1316 if TEST_REPLAY_BLOCKED.with(std::cell::Cell::get) {
1317 return true;
1318 }
1319 REPLAY_BLOCKED.load(std::sync::atomic::Ordering::SeqCst)
1320}
1321
1322#[cfg(all(test, feature = "reporting"))]
1325thread_local! {
1326 static TEST_REPLAY_BLOCKED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1327}
1328
1329#[cfg(feature = "reporting")]
1332fn offline() -> bool {
1333 crate::capsule::effects::current_tape().is_some() || replay_blocked()
1336}
1337
1338#[cfg(feature = "reporting")]
1340const UNCHECKED_REMOVAL_NOTE: &str = "a cache removal with no result reported a failure; \
1341 replay cannot know whether the entry stayed";
1342
1343#[cfg(feature = "reporting")]
1345const FILL_LOCK_NOTE: &str = "a cache fill met a distributed fill lock; the lock outcome \
1346 is not recorded, so replay cannot take the same path";
1347
1348#[cfg(feature = "reporting")]
1350const FILL_FENCE_NOTE: &str = "the shared fill fence stopped a cache fill; replay \
1351 has no fence, so it cannot take the same path";
1352
1353#[cfg(feature = "reporting")]
1355fn recorded_error(
1356 result: &Result<(), InvalidationError>,
1357) -> Option<crate::capsule::CacheInvalidationError> {
1358 result
1359 .as_ref()
1360 .err()
1361 .map(|error| crate::capsule::CacheInvalidationError {
1362 attempts: error.attempts(),
1363 reason: error.reason().to_owned(),
1364 })
1365}
1366
1367#[cfg(feature = "reporting")]
1368impl Cache for CapsuleSeamCache {
1369 fn get_value(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
1373 let direct = !HELPER_CALL.with(std::cell::Cell::get);
1374 if let Some(tape) = crate::capsule::effects::current_tape() {
1378 if direct {
1379 tape.cache_direct_get(key);
1380 }
1381 return None;
1382 }
1383 if replay_blocked() {
1384 return None;
1385 }
1386 if direct && let Some(scope) = crate::capsule::current_scope() {
1387 scope.note(DIRECT_READ_NOTE);
1388 scope.mark_truncated();
1389 }
1390 self.0.get_value(key)
1391 }
1392
1393 fn insert_value(&self, key: &str, value: Arc<dyn Any + Send + Sync>) {
1394 if Self::direct_write_blocked(key) {
1395 return;
1396 }
1397 self.0.insert_value(key, value);
1398 }
1399
1400 fn try_get_value(
1401 &self,
1402 key: &str,
1403 ) -> Result<Option<Arc<dyn Any + Send + Sync>>, CacheBackendError> {
1404 let direct = !HELPER_CALL.with(std::cell::Cell::get);
1405 if let Some(tape) = crate::capsule::effects::current_tape() {
1406 if direct {
1407 tape.cache_direct_get(key);
1408 }
1409 return Ok(None);
1410 }
1411 if replay_blocked() {
1412 return Ok(None);
1413 }
1414 if direct && let Some(scope) = crate::capsule::current_scope() {
1415 scope.note(DIRECT_READ_NOTE);
1416 scope.mark_truncated();
1417 }
1418 self.0.try_get_value(key)
1419 }
1420
1421 fn try_insert_raw_bytes(
1422 &self,
1423 key: &str,
1424 bytes: Vec<u8>,
1425 ttl: Option<Duration>,
1426 ) -> Result<(), CacheBackendError> {
1427 if Self::direct_write_blocked(key) {
1428 return Ok(());
1429 }
1430 self.0.try_insert_raw_bytes(key, bytes, ttl)
1431 }
1432
1433 fn try_invalidate(&self, key: &str) -> Result<(), InvalidationError> {
1434 if let Some(tape) = crate::capsule::effects::current_tape() {
1435 return tape.cache_invalidate(key);
1436 }
1437 if replay_blocked() {
1438 return Ok(());
1439 }
1440 let slot = Self::reserve();
1444 let mut result = Ok(());
1445 Self::unchecked_removal(|| result = self.0.try_invalidate(key));
1446 Self::fill(
1447 slot,
1448 crate::capsule::CacheEffect::Invalidate {
1449 key: key.to_owned(),
1450 error: recorded_error(&result),
1451 },
1452 );
1453 result
1454 }
1455
1456 fn invalidate(&self, key: &str) {
1457 if let Some(tape) = crate::capsule::effects::current_tape() {
1458 let _ = tape.cache_invalidate(key);
1459 return;
1460 }
1461 if replay_blocked() {
1462 return;
1463 }
1464 let slot = Self::reserve();
1467 Self::unchecked_removal(|| self.0.invalidate(key));
1468 Self::fill(
1469 slot,
1470 crate::capsule::CacheEffect::Invalidate {
1471 key: key.to_owned(),
1472 error: None,
1473 },
1474 );
1475 }
1476
1477 fn clear(&self) {
1478 if let Some(tape) = crate::capsule::effects::current_tape() {
1479 tape.cache_clear();
1480 return;
1481 }
1482 if replay_blocked() {
1483 return;
1484 }
1485 let slot = Self::reserve();
1486 Self::unchecked_removal(|| self.0.clear());
1487 Self::fill(slot, crate::capsule::CacheEffect::Clear);
1488 }
1489
1490 fn invalidate_namespace(&self, namespace: &str) -> bool {
1491 if let Some(tape) = crate::capsule::effects::current_tape() {
1492 return tape.cache_invalidate_namespace(namespace).is_ok();
1493 }
1494 if replay_blocked() {
1495 return true;
1496 }
1497 if COHERENCE_REMOVAL.with(std::cell::Cell::get) {
1499 return self.0.invalidate_namespace(namespace);
1500 }
1501 let slot = Self::reserve();
1502 let done = self.0.invalidate_namespace(namespace);
1503 let result = if done {
1504 Ok(())
1505 } else {
1506 Err(InvalidationError::new(
1507 1,
1508 "the backend cannot drop a namespace, or its sweep failed",
1509 ))
1510 };
1511 Self::fill(
1512 slot,
1513 crate::capsule::CacheEffect::InvalidateNamespace {
1514 namespace: namespace.to_owned(),
1515 error: recorded_error(&result),
1516 },
1517 );
1518 done
1519 }
1520
1521 fn insert_raw_bytes(&self, key: &str, bytes: Vec<u8>, ttl: Option<Duration>) {
1522 if Self::direct_write_blocked(key) {
1523 return;
1524 }
1525 self.0.insert_raw_bytes(key, bytes, ttl);
1526 }
1527
1528 fn shares_fill_epoch(&self) -> bool {
1529 self.0.shares_fill_epoch()
1530 }
1531
1532 fn fill_epoch(&self, namespace: &str) -> FillEpoch {
1535 if offline() {
1536 return FillEpoch::Unsupported;
1537 }
1538 let epoch = self.0.fill_epoch(namespace);
1539 if epoch == FillEpoch::Unavailable {
1540 Self::fence_stopped_fill();
1541 }
1542 epoch
1543 }
1544
1545 fn insert_raw_bytes_if_epoch(
1546 &self,
1547 key: &str,
1548 bytes: Vec<u8>,
1549 ttl: Option<Duration>,
1550 namespace: &str,
1551 sampled: u64,
1552 ) -> bool {
1553 if Self::direct_write_blocked(key) {
1554 return false;
1555 }
1556 let stored = self
1557 .0
1558 .insert_raw_bytes_if_epoch(key, bytes, ttl, namespace, sampled);
1559 if !stored {
1560 Self::fence_stopped_fill();
1561 }
1562 stored
1563 }
1564
1565 fn try_acquire_fill_lock(&self, key: &str, token: &str, ttl: Duration) -> FillLockStatus {
1566 if offline() {
1567 return FillLockStatus::Unsupported;
1568 }
1569 let status = self.0.try_acquire_fill_lock(key, token, ttl);
1570 if status != FillLockStatus::Unsupported
1572 && let Some(scope) = crate::capsule::current_scope()
1573 {
1574 scope.note(FILL_LOCK_NOTE);
1575 scope.mark_truncated();
1576 }
1577 status
1578 }
1579
1580 fn release_fill_lock(&self, key: &str, token: &str) {
1581 if offline() {
1582 return;
1583 }
1584 self.0.release_fill_lock(key, token);
1585 }
1586
1587 fn invalidate_async<'a>(
1588 &'a self,
1589 key: &'a str,
1590 ) -> CacheFuture<'a, Result<(), InvalidationError>> {
1591 if let Some(tape) = crate::capsule::effects::current_tape() {
1594 return Box::pin(async move { tape.cache_invalidate(key) });
1595 }
1596 if replay_blocked() {
1597 return Box::pin(async { Ok(()) });
1598 }
1599 let scope = crate::capsule::current_scope();
1604 Box::pin(async move {
1605 let slot = scope.and_then(|scope| scope.reserve_cache().map(|index| (scope, index)));
1606 let result = self.0.invalidate_async(key).await;
1607 if let Some((scope, index)) = slot {
1608 scope.fill_cache(
1609 index,
1610 crate::capsule::CacheEffect::Invalidate {
1611 key: key.to_owned(),
1612 error: recorded_error(&result),
1613 },
1614 );
1615 }
1616 result
1617 })
1618 }
1619
1620 fn invalidate_namespace_async<'a>(
1621 &'a self,
1622 namespace: &'a str,
1623 ) -> CacheFuture<'a, Result<(), InvalidationError>> {
1624 if let Some(tape) = crate::capsule::effects::current_tape() {
1625 return Box::pin(async move { tape.cache_invalidate_namespace(namespace) });
1626 }
1627 if replay_blocked() {
1628 return Box::pin(async { Ok(()) });
1629 }
1630 if COHERENCE_REMOVAL.with(std::cell::Cell::get) {
1632 return self.0.invalidate_namespace_async(namespace);
1633 }
1634 let scope = crate::capsule::current_scope();
1635 Box::pin(async move {
1636 let slot = scope.and_then(|scope| scope.reserve_cache().map(|index| (scope, index)));
1637 let result = self.0.invalidate_namespace_async(namespace).await;
1638 if let Some((scope, index)) = slot {
1639 scope.fill_cache(
1640 index,
1641 crate::capsule::CacheEffect::InvalidateNamespace {
1642 namespace: namespace.to_owned(),
1643 error: recorded_error(&result),
1644 },
1645 );
1646 }
1647 result
1648 })
1649 }
1650
1651 fn is_capsule_seam(&self) -> bool {
1652 true
1653 }
1654}
1655
1656pub trait CacheableResult {
1664 type Ok: Clone;
1666 type Err;
1668
1669 fn into_result(self) -> Result<Self::Ok, Self::Err>;
1675 fn from_ok(ok: Self::Ok) -> Self;
1677}
1678
1679impl<T: Clone, E> CacheableResult for Result<T, E> {
1680 type Ok = T;
1681 type Err = E;
1682
1683 fn into_result(self) -> Self {
1684 self
1685 }
1686
1687 fn from_ok(ok: T) -> Self {
1688 Ok(ok)
1689 }
1690}
1691
1692#[must_use]
1700pub fn make_cache_key<K: Hash>(fn_name: &str, args: &K) -> String {
1701 let mut hasher = DefaultHasher::new();
1702 args.hash(&mut hasher);
1703 format!("{}:{:x}", fn_name, hasher.finish())
1704}
1705
1706#[cfg(test)]
1707mod shared_fence_tests {
1708 use super::*;
1710 use std::collections::HashMap;
1711 use std::sync::Mutex;
1712
1713 #[derive(Default)]
1714 struct Shared {
1715 data: HashMap<String, Vec<u8>>,
1716 epochs: HashMap<String, u64>,
1717 }
1718
1719 #[derive(Clone, Default)]
1720 struct Replica {
1721 shared: Arc<Mutex<Shared>>,
1722 epoch_down: bool,
1723 sweep_fails: bool,
1724 }
1725
1726 impl Cache for Replica {
1727 fn get_value(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
1728 let bytes = self.shared.lock().unwrap().data.get(key).cloned()?;
1729 Some(Arc::new(RawCacheBytes(bytes)))
1730 }
1731 fn insert_value(&self, _key: &str, _value: Arc<dyn Any + Send + Sync>) {
1732 panic!("a shared-fence backend must not take the unfenced path");
1733 }
1734 fn invalidate(&self, key: &str) {
1735 self.shared.lock().unwrap().data.remove(key);
1736 }
1737 fn clear(&self) {
1738 self.shared.lock().unwrap().data.clear();
1739 }
1740 fn invalidate_namespace(&self, namespace: &str) -> bool {
1741 if self.sweep_fails {
1742 return false;
1743 }
1744 let mut shared = self.shared.lock().unwrap();
1745 *shared.epochs.entry(namespace.to_owned()).or_default() += 1;
1746 let prefix = format!("{namespace}:");
1747 shared.data.retain(|key, _| !key.starts_with(&prefix));
1748 true
1749 }
1750 fn insert_raw_bytes(&self, key: &str, bytes: Vec<u8>, _ttl: Option<Duration>) {
1751 self.shared
1752 .lock()
1753 .unwrap()
1754 .data
1755 .insert(key.to_owned(), bytes);
1756 }
1757 fn shares_fill_epoch(&self) -> bool {
1758 true
1759 }
1760 fn fill_epoch(&self, namespace: &str) -> FillEpoch {
1761 if self.epoch_down {
1762 return FillEpoch::Unavailable;
1763 }
1764 let shared = self.shared.lock().unwrap();
1765 FillEpoch::Sampled(shared.epochs.get(namespace).copied().unwrap_or(0))
1766 }
1767 fn insert_raw_bytes_if_epoch(
1768 &self,
1769 key: &str,
1770 bytes: Vec<u8>,
1771 _ttl: Option<Duration>,
1772 namespace: &str,
1773 sampled: u64,
1774 ) -> bool {
1775 let mut shared = self.shared.lock().unwrap();
1776 if shared.epochs.get(namespace).copied().unwrap_or(0) != sampled {
1777 return false;
1778 }
1779 shared.data.insert(key.to_owned(), bytes);
1780 true
1781 }
1782 }
1783
1784 #[test]
1785 fn a_fill_on_another_replica_cannot_resurrect_an_invalidated_value() {
1786 let a = Replica::default();
1787 let b = Replica {
1788 shared: Arc::clone(&a.shared),
1789 epoch_down: false,
1790 sweep_fails: false,
1791 };
1792 let epoch = b.fill_epoch("ns");
1794 assert!(a.invalidate_namespace("ns"));
1796 let inserted = insert_cached_fenced(&b, "ns:k", "old".to_string(), None, "ns", epoch);
1798 assert!(
1799 !inserted,
1800 "the shared epoch moved, so the fill is fenced out"
1801 );
1802 assert!(get_cached::<String>(&a, "ns:k").is_none());
1803 }
1804
1805 #[cfg(feature = "reporting")]
1808 #[test]
1809 fn the_replay_block_keeps_the_fenced_helpers_offline() {
1810 let replica = Replica::default();
1811 let live = replica.fill_epoch("ns");
1812 super::TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(true));
1813 let sampled = sample_fill_epoch(&replica, "ns");
1814 let stored = insert_cached_fenced(&replica, "ns:k", "v".to_string(), None, "ns", live);
1815 super::TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(false));
1816 assert_eq!(sampled, FillEpoch::Unsupported, "no epoch read");
1817 assert!(stored, "the replay took the write");
1818 assert!(replica.shared.lock().unwrap().data.is_empty(), "no write");
1819 }
1820
1821 #[cfg(feature = "reporting")]
1825 #[test]
1826 fn a_replay_keeps_the_fenced_fill_result() {
1827 struct Unencodable;
1828 impl serde::Serialize for Unencodable {
1829 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
1830 Err(serde::ser::Error::custom("cannot encode"))
1831 }
1832 }
1833 impl Clone for Unencodable {
1834 fn clone(&self) -> Self {
1835 Self
1836 }
1837 }
1838 let replica = Replica::default();
1839 super::TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(true));
1840 let unencodable = insert_cached_fenced(
1841 &replica,
1842 "ns:k",
1843 Unencodable,
1844 None,
1845 "ns",
1846 FillEpoch::Sampled(0),
1847 );
1848 let unavailable =
1849 insert_cached_fenced(&replica, "ns:k", 1_u32, None, "ns", FillEpoch::Unavailable);
1850 super::TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(false));
1851 assert!(!unencodable, "the fenced path does not store it");
1852 assert!(!unavailable, "an unreadable epoch stores nothing");
1853 assert!(replica.shared.lock().unwrap().data.is_empty());
1854 }
1855
1856 #[test]
1857 fn a_fill_inserts_when_no_invalidation_landed() {
1858 let a = Replica::default();
1859 let epoch = a.fill_epoch("ns");
1860 assert!(insert_cached_fenced(
1861 &a,
1862 "ns:k",
1863 "new".to_string(),
1864 None,
1865 "ns",
1866 epoch
1867 ));
1868 assert_eq!(get_cached::<String>(&a, "ns:k").as_deref(), Some("new"));
1869 }
1870
1871 #[test]
1872 fn an_unreadable_epoch_skips_the_insert() {
1873 let a = Replica {
1874 epoch_down: true,
1875 ..Replica::default()
1876 };
1877 let epoch = a.fill_epoch("ns");
1878 assert_eq!(epoch, FillEpoch::Unavailable);
1879 assert!(!insert_cached_fenced(
1880 &a,
1881 "ns:k",
1882 "v".to_string(),
1883 None,
1884 "ns",
1885 epoch
1886 ));
1887 assert!(get_cached::<String>(&a, "ns:k").is_none());
1888 }
1889
1890 #[test]
1891 fn a_fragment_render_on_another_replica_cannot_resurrect_an_invalidated_value() {
1892 let a = Replica::default();
1893 let b = Replica {
1894 shared: Arc::clone(&a.shared),
1895 epoch_down: false,
1896 sweep_fails: false,
1897 };
1898 let html = fragment::cache_fragment_in(Some(&b), "ns", "id", "v1", None, || {
1899 assert!(a.invalidate_namespace("ns"));
1901 maud::html! { "old" }
1902 });
1903 assert_eq!(html.0, "old", "the fenced-out caller still gets its markup");
1904 let again = fragment::cache_fragment_in(Some(&b), "ns", "id", "v1", None, || {
1905 maud::html! { "new" }
1906 });
1907 assert_eq!(again.0, "new", "the stale markup must not be cached");
1908 }
1909
1910 #[test]
1914 fn invalidating_a_registered_shared_store_raises_its_epoch() {
1915 let _guard = GLOBAL_CACHE_TEST_LOCK
1916 .lock()
1917 .unwrap_or_else(std::sync::PoisonError::into_inner);
1918 clear_global_cache();
1919 let store = Replica::default();
1920 let handle = Arc::new(store.clone());
1921 let _ = coherence::register_namespace_store("tests::shared_fence_ns", handle);
1922 let before = store.fill_epoch("tests::shared_fence_ns");
1923 assert!(coherence::invalidate_namespace("tests::shared_fence_ns"));
1924 assert_ne!(
1925 store.fill_epoch("tests::shared_fence_ns"),
1926 before,
1927 "an in-flight fill on another replica must be fenced out"
1928 );
1929 }
1930
1931 #[test]
1934 fn a_failed_registered_shared_store_fails_the_invalidation() {
1935 let _guard = GLOBAL_CACHE_TEST_LOCK
1936 .lock()
1937 .unwrap_or_else(std::sync::PoisonError::into_inner);
1938 clear_global_cache();
1939 let failing = Replica {
1940 sweep_fails: true,
1941 ..Replica::default()
1942 };
1943 let _ = coherence::register_namespace_store("tests::failing_shared_ns", Arc::new(failing));
1944 assert!(!coherence::invalidate_namespace("tests::failing_shared_ns"));
1945 }
1946
1947 #[cfg(feature = "cache-moka")]
1948 #[test]
1949 fn a_registered_store_without_a_shared_epoch_still_counts_as_cleared() {
1950 let _guard = GLOBAL_CACHE_TEST_LOCK
1951 .lock()
1952 .unwrap_or_else(std::sync::PoisonError::into_inner);
1953 clear_global_cache();
1954 let _ = coherence::register_namespace_store(
1955 "tests::plain_ns",
1956 Arc::new(MokaCache::new(4, None)),
1957 );
1958 assert!(coherence::invalidate_namespace("tests::plain_ns"));
1959 }
1960
1961 #[cfg(feature = "reporting")]
1964 #[tokio::test]
1965 async fn a_replay_never_touches_the_live_epoch_and_still_writes_the_tape() {
1966 use crate::capsule::{
1967 CapsuleEffects,
1968 effects::{CachedValue, ReplayEffects},
1969 };
1970 let live = Replica {
1971 epoch_down: true,
1972 ..Replica::default()
1973 };
1974 let tape = std::sync::Arc::new(ReplayEffects::new(CapsuleEffects::default()));
1975 let seen = std::sync::Arc::clone(&tape);
1976 crate::capsule::effects::with_effect_tape(tape, async {
1977 let epoch = sample_fill_epoch(&live, "ns");
1978 assert_eq!(
1979 epoch,
1980 FillEpoch::Unsupported,
1981 "replay must skip the backend"
1982 );
1983 assert!(insert_cached_fenced(
1984 &live, "ns:k", 7_u32, None, "ns", epoch
1985 ));
1986 })
1987 .await;
1988 assert!(
1989 live.shared.lock().unwrap().data.is_empty(),
1990 "a replay must not write the live backend"
1991 );
1992 assert!(matches!(seen.cache_get("ns:k"), CachedValue::Hit(_)));
1993 }
1994
1995 #[cfg(feature = "reporting")]
1996 fn capture_scope() -> Arc<crate::capsule::CaptureScope> {
1997 Arc::new(crate::capsule::CaptureScope::new(
1998 "fence-test".to_owned(),
1999 Arc::new(crate::capsule::CaptureSettings::default()),
2000 Arc::new(crate::log::filter::ParameterFilter::default()),
2001 ))
2002 }
2003
2004 #[cfg(feature = "reporting")]
2007 #[test]
2008 fn the_capsule_seam_keeps_the_shared_fence() {
2009 let a = Replica::default();
2010 let b = with_capsule_seam(Arc::new(Replica {
2011 shared: Arc::clone(&a.shared),
2012 ..Replica::default()
2013 }));
2014 assert!(b.shares_fill_epoch());
2015 let epoch = sample_fill_epoch(b.as_ref(), "ns");
2016 assert_eq!(epoch, FillEpoch::Sampled(0));
2017 assert!(a.invalidate_namespace("ns"));
2018 assert!(!insert_cached_fenced(
2019 b.as_ref(),
2020 "ns:k",
2021 "old".to_string(),
2022 None,
2023 "ns",
2024 epoch
2025 ));
2026 assert!(a.shared.lock().unwrap().data.is_empty());
2027 }
2028
2029 #[cfg(feature = "reporting")]
2033 #[test]
2034 fn a_fill_the_fence_stops_marks_the_capsule_incomplete() {
2035 use futures::executor::block_on;
2036 let fill = |replica: Replica, invalidate: bool| {
2037 let shared = Arc::clone(&replica.shared);
2038 let cache = with_capsule_seam(Arc::new(replica));
2039 let scope = capture_scope();
2040 block_on(crate::capsule::capture::with_capture_scope(
2041 Arc::clone(&scope),
2042 async {
2043 let epoch = sample_fill_epoch(cache.as_ref(), "ns");
2044 if invalidate {
2045 shared.lock().unwrap().epochs.insert("ns".to_owned(), 9);
2046 }
2047 let _ = insert_cached_fenced(cache.as_ref(), "ns:k", 1_u32, None, "ns", epoch);
2048 },
2049 ));
2050 scope
2051 };
2052 let stored = fill(Replica::default(), false);
2053 assert!(!stored.is_truncated(), "a stored fill is replayable");
2054 assert_eq!(stored.effects_snapshot().cache.len(), 1);
2055 assert!(fill(Replica::default(), true).is_truncated());
2056 let down = Replica {
2057 epoch_down: true,
2058 ..Replica::default()
2059 };
2060 assert!(fill(down, false).is_truncated());
2061 }
2062
2063 #[cfg(feature = "reporting")]
2065 #[test]
2066 fn a_direct_fenced_write_marks_the_capsule_incomplete() {
2067 use futures::executor::block_on;
2068 let cache = with_capsule_seam(Arc::new(Replica::default()));
2069 let scope = capture_scope();
2070 block_on(crate::capsule::capture::with_capture_scope(
2071 Arc::clone(&scope),
2072 async {
2073 let _ = cache.insert_raw_bytes_if_epoch("ns:k", b"1".to_vec(), None, "ns", 0);
2074 },
2075 ));
2076 assert!(scope.is_truncated());
2077 }
2078
2079 #[cfg(feature = "cache-moka")]
2080 #[test]
2081 fn a_backend_without_a_shared_epoch_inserts_as_before() {
2082 let moka = MokaCache::new(10, None);
2083 let epoch = moka.fill_epoch("ns");
2084 assert_eq!(epoch, FillEpoch::Unsupported);
2085 assert!(insert_cached_fenced(
2086 &moka,
2087 "ns:k",
2088 "v".to_string(),
2089 None,
2090 "ns",
2091 epoch
2092 ));
2093 assert_eq!(get_cached::<String>(&moka, "ns:k").as_deref(), Some("v"));
2094 }
2095}
2096
2097#[cfg(test)]
2098mod tests {
2099 use super::*;
2100
2101 #[test]
2102 fn cache_key_deterministic() {
2103 let k1 = make_cache_key("get_user", &(42_i64,));
2104 let k2 = make_cache_key("get_user", &(42_i64,));
2105 assert_eq!(k1, k2);
2106 }
2107
2108 #[test]
2109 fn cache_key_differs_by_fn_name() {
2110 let k1 = make_cache_key("get_user", &(42_i64,));
2111 let k2 = make_cache_key("find_user", &(42_i64,));
2112 assert_ne!(k1, k2);
2113 }
2114
2115 #[test]
2116 fn cache_key_differs_by_args() {
2117 let k1 = make_cache_key("get_user", &(1_i64,));
2118 let k2 = make_cache_key("get_user", &(2_i64,));
2119 assert_ne!(k1, k2);
2120 }
2121
2122 #[test]
2123 fn cache_key_no_args() {
2124 let k = make_cache_key("get_config", &());
2125 assert!(k.starts_with("get_config:"));
2126 }
2127
2128 #[cfg(feature = "cache-moka")]
2129 #[test]
2130 fn insert_cached_and_get_cached_round_trip() {
2131 let cache = MokaCache::new(10, None);
2132 insert_cached(&cache, "key", "hello".to_string(), None);
2133 let val: Option<String> = get_cached(&cache, "key");
2134 assert_eq!(val.as_deref(), Some("hello"));
2135 }
2136
2137 #[cfg(feature = "cache-moka")]
2138 #[test]
2139 fn get_cached_raw_bytes_slow_path() {
2140 let cache = MokaCache::new(10, None);
2143 let bytes = serde_json::to_vec(&42_i32).unwrap();
2144 cache.insert_value("k", Arc::new(RawCacheBytes(bytes)));
2145 let val: Option<i32> = get_cached(&cache, "k");
2146 assert_eq!(val, Some(42));
2147 }
2148
2149 #[cfg(feature = "cache-moka")]
2150 #[test]
2151 fn get_cached_miss_returns_none() {
2152 let cache = MokaCache::new(10, None);
2153 let val: Option<String> = get_cached(&cache, "missing");
2154 assert!(val.is_none());
2155 }
2156
2157 struct MinimalBackend {
2159 invalidated: std::sync::Mutex<Vec<String>>,
2160 }
2161
2162 impl Cache for MinimalBackend {
2163 fn get_value(&self, _key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2164 None
2165 }
2166 fn insert_value(&self, _key: &str, _value: Arc<dyn Any + Send + Sync>) {}
2167 fn invalidate(&self, key: &str) {
2168 self.invalidated
2169 .lock()
2170 .unwrap_or_else(std::sync::PoisonError::into_inner)
2171 .push(key.to_owned());
2172 }
2173 fn clear(&self) {}
2174 }
2175
2176 #[test]
2177 fn default_try_methods_delegate_and_succeed() {
2178 let backend = MinimalBackend {
2179 invalidated: std::sync::Mutex::new(Vec::new()),
2180 };
2181 assert!(matches!(backend.try_get_value("k"), Ok(None)));
2182 assert!(backend.try_insert_raw_bytes("k", vec![1], None).is_ok());
2183 backend
2184 .try_invalidate("k")
2185 .expect("the default cannot fail");
2186 assert_eq!(
2187 *backend
2188 .invalidated
2189 .lock()
2190 .unwrap_or_else(std::sync::PoisonError::into_inner),
2191 ["k"],
2192 "the default try_invalidate must reach invalidate"
2193 );
2194 }
2195
2196 struct DownBackend;
2198
2199 impl Cache for DownBackend {
2200 fn get_value(&self, _key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2201 None
2202 }
2203 fn insert_value(&self, _key: &str, _value: Arc<dyn Any + Send + Sync>) {}
2204 fn invalidate(&self, _key: &str) {}
2205 fn clear(&self) {}
2206 fn try_get_value(
2207 &self,
2208 _key: &str,
2209 ) -> Result<Option<Arc<dyn Any + Send + Sync>>, CacheBackendError> {
2210 Err(CacheBackendError::new("down"))
2211 }
2212 fn try_insert_raw_bytes(
2213 &self,
2214 _key: &str,
2215 _bytes: Vec<u8>,
2216 _ttl: Option<Duration>,
2217 ) -> Result<(), CacheBackendError> {
2218 Err(CacheBackendError::new("down"))
2219 }
2220 fn try_invalidate(&self, _key: &str) -> Result<(), InvalidationError> {
2221 Err(InvalidationError::new(1, "down"))
2222 }
2223 }
2224
2225 #[cfg(feature = "reporting")]
2228 #[test]
2229 fn the_capsule_seam_passes_try_failures_through() {
2230 let cache = with_capsule_seam(Arc::new(DownBackend));
2231 for op in [
2232 (|c: &dyn Cache| assert!(try_insert_cached(c, "k", 1_i64, None).is_err()))
2233 as fn(&dyn Cache),
2234 |c| assert!(try_get_cached::<i64>(c, "k").is_err()),
2235 ] {
2236 let scope = capture_scope();
2238 block_on(crate::capsule::capture::with_capture_scope(
2239 Arc::clone(&scope),
2240 async { op(cache.as_ref()) },
2241 ));
2242 assert!(scope.is_truncated(), "a failed cache op is not replayable");
2243 }
2244 let scope = capture_scope();
2246 block_on(crate::capsule::capture::with_capture_scope(
2247 Arc::clone(&scope),
2248 async { assert!(cache.try_invalidate("k").is_err()) },
2249 ));
2250 let _ = scope.effects_snapshot();
2251 assert!(!scope.is_truncated(), "a recorded removal error replays");
2252
2253 let direct = capture_scope();
2255 block_on(crate::capsule::capture::with_capture_scope(
2256 Arc::clone(&direct),
2257 async {
2258 let _ = cache.try_get_value("k");
2259 },
2260 ));
2261 assert!(direct.is_truncated(), "a direct read is not recorded");
2262 }
2263
2264 #[derive(Clone)]
2266 struct Unserializable;
2267
2268 impl serde::Serialize for Unserializable {
2269 fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
2270 Err(serde::ser::Error::custom("no"))
2271 }
2272 }
2273
2274 #[test]
2275 fn try_insert_cached_reports_a_serialization_failure() {
2276 let cache = MokaCache::new(8, None);
2277 let err = try_insert_cached(&cache, "k", Unserializable, None).expect_err("no bytes");
2278 assert!(err.reason().contains("cannot be serialized"));
2279 }
2280
2281 #[test]
2282 fn try_cached_helpers_report_backend_failure() {
2283 let err = try_insert_cached(&DownBackend, "k", 1_i64, None).expect_err("write failed");
2284 assert_eq!(err.reason(), "down");
2285 let err = try_get_cached::<i64>(&DownBackend, "k").expect_err("read failed");
2286 assert_eq!(err.reason(), "down");
2287 }
2288
2289 struct CorruptBackend;
2291
2292 impl Cache for CorruptBackend {
2293 fn get_value(&self, _key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2294 None
2295 }
2296 fn insert_value(&self, _key: &str, _value: Arc<dyn Any + Send + Sync>) {}
2297 fn invalidate(&self, _key: &str) {}
2298 fn clear(&self) {}
2299 fn try_get_value(
2300 &self,
2301 _key: &str,
2302 ) -> Result<Option<Arc<dyn Any + Send + Sync>>, CacheBackendError> {
2303 Ok(Some(Arc::new(RawCacheBytes(b"not json".to_vec()))))
2304 }
2305 }
2306
2307 #[test]
2308 fn try_get_cached_reports_bytes_it_cannot_decode() {
2309 let err = try_get_cached::<i64>(&CorruptBackend, "k").expect_err("corrupt bytes");
2310 assert!(err.reason().contains("cannot be decoded"), "{err}");
2311 }
2312
2313 #[cfg(feature = "reporting")]
2315 #[test]
2316 fn a_decode_fault_marks_the_capsule_incomplete() {
2317 let scope = capture_scope();
2318 block_on(crate::capsule::capture::with_capture_scope(
2319 Arc::clone(&scope),
2320 async {
2321 assert!(try_get_cached::<i64>(&CorruptBackend, "k").is_err());
2322 },
2323 ));
2324 assert!(scope.is_truncated());
2325 }
2326
2327 #[test]
2328 fn try_cached_helpers_round_trip_in_process() {
2329 let cache = MokaCache::new(8, None);
2330 try_insert_cached(&cache, "k", 7_i64, None).expect("stored");
2331 assert_eq!(try_get_cached::<i64>(&cache, "k").expect("read"), Some(7));
2332 assert_eq!(try_get_cached::<i64>(&cache, "gone").expect("read"), None);
2333 }
2334
2335 fn block_on<F: std::future::Future>(future: F) -> F::Output {
2336 tokio::runtime::Builder::new_current_thread()
2337 .build()
2338 .expect("runtime")
2339 .block_on(future)
2340 }
2341
2342 #[test]
2343 fn default_invalidate_async_calls_the_sync_method() {
2344 let backend = MinimalBackend {
2345 invalidated: std::sync::Mutex::new(Vec::new()),
2346 };
2347 block_on(backend.invalidate_async("k")).expect("the default cannot fail");
2348 assert_eq!(
2349 *backend
2350 .invalidated
2351 .lock()
2352 .unwrap_or_else(std::sync::PoisonError::into_inner),
2353 vec!["k".to_owned()]
2354 );
2355 }
2356
2357 #[test]
2358 fn default_invalidate_namespace_async_reports_an_unsupported_backend() {
2359 let backend = MinimalBackend {
2360 invalidated: std::sync::Mutex::new(Vec::new()),
2361 };
2362 let err = block_on(backend.invalidate_namespace_async("ns"))
2363 .expect_err("a backend that cannot sweep must say so");
2364 assert_eq!(err.attempts(), 1);
2365 }
2366
2367 #[cfg(feature = "reporting")]
2369 #[derive(Default)]
2370 struct SpyBackend {
2371 calls: std::sync::Mutex<Vec<String>>,
2372 }
2373
2374 #[cfg(feature = "reporting")]
2375 impl SpyBackend {
2376 fn log(&self, call: impl Into<String>) {
2377 self.calls
2378 .lock()
2379 .unwrap_or_else(std::sync::PoisonError::into_inner)
2380 .push(call.into());
2381 }
2382 fn calls(&self) -> Vec<String> {
2383 self.calls
2384 .lock()
2385 .unwrap_or_else(std::sync::PoisonError::into_inner)
2386 .clone()
2387 }
2388 }
2389
2390 #[cfg(feature = "reporting")]
2391 impl Cache for SpyBackend {
2392 fn get_value(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2393 self.log(format!("get {key}"));
2394 Some(Arc::new(7_u32))
2395 }
2396 fn insert_value(&self, key: &str, _value: Arc<dyn Any + Send + Sync>) {
2397 self.log(format!("insert {key}"));
2398 }
2399 fn invalidate(&self, key: &str) {
2400 self.log(format!("invalidate {key}"));
2401 }
2402 fn clear(&self) {
2403 self.log("clear");
2404 }
2405 fn invalidate_namespace(&self, namespace: &str) -> bool {
2406 self.log(format!("invalidate_namespace {namespace}"));
2407 true
2408 }
2409 }
2410
2411 #[cfg(feature = "reporting")]
2414 #[test]
2415 fn a_replayed_run_never_reaches_the_installed_backend() {
2416 let spy = Arc::new(SpyBackend::default());
2417 let state =
2418 crate::state::AppState::for_test().with_cache(Arc::clone(&spy) as Arc<dyn Cache>);
2419 let tape = Arc::new(crate::capsule::ReplayEffects::new(
2420 crate::capsule::CapsuleEffects::default(),
2421 ));
2422 block_on(crate::capsule::with_effect_tape(Arc::clone(&tape), async {
2423 let cache = state.cache().expect("installed");
2424 cache.invalidate("widgets");
2425 cache.clear();
2426 let _ = cache.invalidate_namespace("widgets");
2427 let _ = cache.invalidate_async("widgets").await;
2428 insert(cache.as_ref(), "widgets", 41_u32);
2429 assert_eq!(get::<u32>(cache.as_ref(), "widgets"), None);
2430 }));
2431 assert!(spy.calls().is_empty(), "{:?}", spy.calls());
2432 assert_eq!(tape.divergences().len(), 6, "{:?}", tape.divergences());
2433 }
2434
2435 #[cfg(feature = "reporting")]
2437 #[test]
2438 fn a_captured_removal_is_recorded_and_still_applied() {
2439 let spy = Arc::new(SpyBackend::default());
2440 let state =
2441 crate::state::AppState::for_test().with_cache(Arc::clone(&spy) as Arc<dyn Cache>);
2442 let scope = Arc::new(crate::capsule::CaptureScope::new(
2443 "cache-test".to_owned(),
2444 Arc::new(crate::capsule::CaptureSettings::default()),
2445 Arc::new(crate::log::filter::ParameterFilter::default()),
2446 ));
2447 block_on(crate::capsule::capture::with_capture_scope(
2448 Arc::clone(&scope),
2449 async {
2450 let cache = state.cache().expect("installed");
2451 cache.invalidate("widgets");
2452 cache.clear();
2453 },
2454 ));
2455 assert_eq!(spy.calls(), vec!["invalidate widgets", "clear"]);
2456 assert_eq!(
2457 scope.effects_snapshot().cache,
2458 vec![
2459 crate::capsule::CacheEffect::Invalidate {
2460 key: "widgets".to_owned(),
2461 error: None,
2462 },
2463 crate::capsule::CacheEffect::Clear,
2464 ]
2465 );
2466 }
2467
2468 #[cfg(feature = "reporting")]
2471 #[test]
2472 fn a_captured_untyped_hit_or_write_marks_the_capsule_incomplete() {
2473 let spy = Arc::new(SpyBackend::default());
2474 let scope = Arc::new(crate::capsule::CaptureScope::new(
2475 "cache-test".to_owned(),
2476 Arc::new(crate::capsule::CaptureSettings::default()),
2477 Arc::new(crate::log::filter::ParameterFilter::default()),
2478 ));
2479 block_on(crate::capsule::capture::with_capture_scope(
2480 Arc::clone(&scope),
2481 async {
2482 assert_eq!(get::<u32>(spy.as_ref(), "widgets"), Some(7));
2483 },
2484 ));
2485 assert!(scope.is_truncated());
2486 let scope = Arc::new(crate::capsule::CaptureScope::new(
2487 "cache-test".to_owned(),
2488 Arc::new(crate::capsule::CaptureSettings::default()),
2489 Arc::new(crate::log::filter::ParameterFilter::default()),
2490 ));
2491 block_on(crate::capsule::capture::with_capture_scope(
2492 Arc::clone(&scope),
2493 async {
2494 insert(spy.as_ref(), "widgets", 1_u32);
2495 },
2496 ));
2497 assert!(scope.is_truncated());
2498 }
2499
2500 #[cfg(feature = "reporting")]
2504 #[test]
2505 fn a_direct_read_on_replay_is_a_divergence_not_a_helper_read() {
2506 let cache = with_capsule_seam(Arc::new(SpyBackend::default()) as Arc<dyn Cache>);
2507 let recorded = crate::capsule::CapsuleEffects {
2508 cache: vec![crate::capsule::CacheEffect::Get {
2509 key: "user:7".to_owned(),
2510 value: None,
2511 }],
2512 ..Default::default()
2513 };
2514 let tape = Arc::new(crate::capsule::ReplayEffects::new(recorded));
2515 block_on(crate::capsule::with_effect_tape(Arc::clone(&tape), async {
2516 assert!(cache.get_value("user:7").is_none());
2517 }));
2518 let finished = tape.finish();
2519 assert!(
2520 finished
2521 .iter()
2522 .any(|divergence| divergence.actual.contains("direct cache read")),
2523 "{finished:?}"
2524 );
2525 assert!(
2526 !finished
2527 .iter()
2528 .any(|divergence| divergence.actual.contains("user:7")),
2529 "the key is withheld: {finished:?}"
2530 );
2531 assert!(
2532 finished
2533 .iter()
2534 .any(|divergence| divergence.kind
2535 == crate::capsule::EffectDivergenceKind::Unconsumed),
2536 "the helper's recorded read is still owed: {finished:?}"
2537 );
2538 }
2539
2540 #[cfg(feature = "reporting")]
2543 #[test]
2544 fn the_wrapper_reaches_no_backend_during_a_replay() {
2545 let spy = Arc::new(SpyBackend::default());
2546 let cache = with_capsule_seam(Arc::clone(&spy) as Arc<dyn Cache>);
2547 let tape = Arc::new(crate::capsule::ReplayEffects::new(
2548 crate::capsule::CapsuleEffects::default(),
2549 ));
2550 block_on(crate::capsule::with_effect_tape(Arc::clone(&tape), async {
2551 assert!(cache.get_value("k").is_none());
2552 cache.insert_value("k", Arc::new(1_u32));
2553 cache.insert_raw_bytes("k", b"1".to_vec(), None);
2554 assert_eq!(
2555 cache.try_acquire_fill_lock("k", "t", Duration::from_secs(1)),
2556 FillLockStatus::Unsupported
2557 );
2558 cache.release_fill_lock("k", "t");
2559 assert_eq!(cache.fill_epoch("ns"), FillEpoch::Unsupported);
2560 }));
2561 assert!(spy.calls().is_empty(), "{:?}", spy.calls());
2562 assert!(!tape.scope_entered());
2565 }
2566
2567 #[cfg(feature = "reporting")]
2570 #[test]
2571 fn an_async_removal_is_recorded_when_it_first_runs() {
2572 let spy = Arc::new(SpyBackend::default());
2573 let cache = with_capsule_seam(Arc::clone(&spy) as Arc<dyn Cache>);
2574 let scope = Arc::new(crate::capsule::CaptureScope::new(
2575 "cache-test".to_owned(),
2576 Arc::new(crate::capsule::CaptureSettings::default()),
2577 Arc::new(crate::log::filter::ParameterFilter::default()),
2578 ));
2579 block_on(crate::capsule::capture::with_capture_scope(
2580 Arc::clone(&scope),
2581 async {
2582 let first = cache.invalidate_async("a");
2583 cache.clear();
2584 first.await.expect("removed");
2585 },
2586 ));
2587 assert_eq!(
2588 scope.effects_snapshot().cache,
2589 vec![
2590 crate::capsule::CacheEffect::Clear,
2591 crate::capsule::CacheEffect::Invalidate {
2592 key: "a".to_owned(),
2593 error: None,
2594 },
2595 ]
2596 );
2597 }
2598
2599 #[cfg(feature = "reporting")]
2602 #[test]
2603 fn replay_installs_a_seam_cache_when_the_builder_had_one() {
2604 let _guard = GLOBAL_CACHE_TEST_LOCK
2605 .lock()
2606 .unwrap_or_else(std::sync::PoisonError::into_inner);
2607 let recorded = crate::capsule::CapsuleEffects {
2608 cache: vec![crate::capsule::CacheEffect::InvalidateNamespace {
2609 namespace: "posts".to_owned(),
2610 error: None,
2611 }],
2612 state_cache: true,
2613 ..Default::default()
2614 };
2615 let installed = install_replay_cache(true).expect("installed");
2616 assert!(installed.is_capsule_seam());
2617 assert!(Arc::ptr_eq(
2618 &installed,
2619 &global_cache().expect("the global cache")
2620 ));
2621 let tape = Arc::new(crate::capsule::ReplayEffects::new(recorded));
2622 let complete = block_on(crate::capsule::with_effect_tape(
2623 Arc::clone(&tape),
2624 coherence::invalidate_namespace_async("posts"),
2625 ));
2626 clear_global_cache();
2627 assert!(complete);
2628 assert!(tape.finish().is_empty(), "{:?}", tape.finish());
2629
2630 assert!(install_replay_cache(false).is_none());
2631 assert!(global_cache().is_none());
2632 }
2633
2634 #[cfg(feature = "reporting")]
2638 #[test]
2639 fn a_startup_hook_cache_is_installed_after_the_initializers() {
2640 let _guard = GLOBAL_CACHE_TEST_LOCK
2641 .lock()
2642 .unwrap_or_else(std::sync::PoisonError::into_inner);
2643 let recorded = crate::capsule::CapsuleEffects {
2644 state_cache: true,
2645 ..Default::default()
2646 };
2647 let state = crate::state::AppState::for_test();
2648 install_late_replay_cache(&state, &recorded);
2649 let late = state.cache().expect("installed after the initializers");
2650 assert!(late.is_capsule_seam());
2651
2652 let initializer = crate::state::AppState::for_test();
2653 initializer.set_cache(Arc::new(SpyBackend::default()));
2654 let kept = initializer.cache().expect("the initializer's cache");
2655 install_late_replay_cache(&initializer, &recorded);
2656 assert!(Arc::ptr_eq(
2657 &kept,
2658 &initializer.cache().expect("still there")
2659 ));
2660
2661 let none = crate::state::AppState::for_test();
2662 install_late_replay_cache(&none, &crate::capsule::CapsuleEffects::default());
2663 clear_global_cache();
2664 assert!(none.cache().is_none());
2665 }
2666
2667 #[cfg(feature = "reporting")]
2669 struct FailingSyncBackend;
2670
2671 #[cfg(feature = "reporting")]
2672 impl Cache for FailingSyncBackend {
2673 fn get_value(&self, _key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2674 None
2675 }
2676 fn insert_value(&self, _key: &str, _value: Arc<dyn Any + Send + Sync>) {}
2677 fn invalidate(&self, _key: &str) {
2678 record_invalidation_failure();
2679 }
2680 fn clear(&self) {
2681 record_invalidation_failure();
2682 }
2683 fn try_acquire_fill_lock(
2684 &self,
2685 _key: &str,
2686 _token: &str,
2687 _ttl: Duration,
2688 ) -> FillLockStatus {
2689 FillLockStatus::Held
2690 }
2691 }
2692
2693 #[cfg(feature = "reporting")]
2694 fn capture_scope() -> Arc<crate::capsule::CaptureScope> {
2695 Arc::new(crate::capsule::CaptureScope::new(
2696 "cache-test".to_owned(),
2697 Arc::new(crate::capsule::CaptureSettings::default()),
2698 Arc::new(crate::log::filter::ParameterFilter::default()),
2699 ))
2700 }
2701
2702 #[cfg(feature = "reporting")]
2705 #[test]
2706 fn a_failed_sync_removal_marks_the_capsule_incomplete() {
2707 for remove in [
2708 (|cache: &dyn Cache| cache.invalidate("k")) as fn(&dyn Cache),
2709 |cache: &dyn Cache| cache.clear(),
2710 ] {
2711 let cache = with_capsule_seam(Arc::new(FailingSyncBackend));
2712 let scope = capture_scope();
2713 block_on(crate::capsule::capture::with_capture_scope(
2714 Arc::clone(&scope),
2715 async { remove(cache.as_ref()) },
2716 ));
2717 assert!(scope.is_truncated());
2718 }
2719 }
2720
2721 #[cfg(feature = "reporting")]
2724 #[test]
2725 fn a_distributed_fill_lock_marks_the_capsule_incomplete() {
2726 let cache = with_capsule_seam(Arc::new(FailingSyncBackend));
2727 let scope = capture_scope();
2728 let status = block_on(crate::capsule::capture::with_capture_scope(
2729 Arc::clone(&scope),
2730 async { cache.try_acquire_fill_lock("k", "t", Duration::from_secs(1)) },
2731 ));
2732 assert_eq!(status, FillLockStatus::Held);
2733 assert!(scope.is_truncated());
2734
2735 let moka = with_capsule_seam(Arc::new(SpyBackend::default()));
2736 let scope = capture_scope();
2737 block_on(crate::capsule::capture::with_capture_scope(
2738 Arc::clone(&scope),
2739 async {
2740 let _ = moka.try_acquire_fill_lock("k", "t", Duration::from_secs(1));
2741 },
2742 ));
2743 assert!(
2744 !scope.is_truncated(),
2745 "a backend with no lock is replayable"
2746 );
2747 }
2748
2749 #[cfg(feature = "reporting")]
2753 #[test]
2754 fn a_cache_from_any_install_route_is_recorded() {
2755 let _guard = GLOBAL_CACHE_TEST_LOCK
2756 .lock()
2757 .unwrap_or_else(std::sync::PoisonError::into_inner);
2758 let builder = crate::state::AppState::for_test()
2759 .with_cache(Arc::new(SpyBackend::default()) as Arc<dyn Cache>);
2760 assert!(builder.has_cache());
2761 let startup = crate::state::AppState::for_test();
2762 assert!(!startup.has_cache());
2763 startup.set_cache(Arc::new(SpyBackend::default()));
2764 clear_global_cache();
2765 assert!(startup.has_cache());
2766 }
2767
2768 #[cfg(feature = "reporting")]
2771 #[test]
2772 fn the_replay_block_keeps_the_backend_offline_with_no_tape() {
2773 let spy = Arc::new(SpyBackend::default());
2774 let cache = with_capsule_seam(Arc::clone(&spy) as Arc<dyn Cache>);
2775 TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(true));
2776 block_on(async {
2777 assert!(cache.get_value("k").is_none());
2778 cache.insert_value("k", Arc::new(1_u32));
2779 cache.insert_raw_bytes("k", b"1".to_vec(), None);
2780 cache.invalidate("k");
2781 cache.clear();
2782 assert!(cache.invalidate_namespace("ns"));
2783 assert!(cache.invalidate_async("k").await.is_ok());
2784 assert!(cache.invalidate_namespace_async("ns").await.is_ok());
2785 assert_eq!(
2786 cache.try_acquire_fill_lock("k", "t", Duration::from_secs(1)),
2787 FillLockStatus::Unsupported
2788 );
2789 });
2790 TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(false));
2791 assert!(spy.calls().is_empty(), "{:?}", spy.calls());
2792 }
2793
2794 #[cfg(feature = "reporting")]
2797 #[test]
2798 fn the_replay_block_keeps_a_raw_backend_offline_in_the_helpers() {
2799 let spy = SpyBackend::default();
2800 TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(true));
2801 assert_eq!(get::<u32>(&spy, "k"), None);
2802 insert(&spy, "k", 1_u32);
2803 assert_eq!(get_cached::<u32>(&spy, "k"), None);
2804 insert_cached(&spy, "k", 1_u32, None);
2805 TEST_REPLAY_BLOCKED.with(|blocked| blocked.set(false));
2806 assert!(spy.calls().is_empty(), "{:?}", spy.calls());
2807 }
2808
2809 #[cfg(feature = "reporting")]
2811 struct PanickingBackend;
2812
2813 #[cfg(feature = "reporting")]
2814 impl Cache for PanickingBackend {
2815 fn get_value(&self, _key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
2816 None
2817 }
2818 fn insert_value(&self, _key: &str, _value: Arc<dyn Any + Send + Sync>) {}
2819 fn invalidate(&self, _key: &str) {
2820 panic!("backend failed");
2821 }
2822 fn clear(&self) {
2823 panic!("backend failed");
2824 }
2825 fn invalidate_namespace(&self, _namespace: &str) -> bool {
2826 panic!("backend failed");
2827 }
2828 }
2829
2830 #[cfg(feature = "reporting")]
2834 #[test]
2835 fn a_panicking_sync_removal_leaves_the_capsule_incomplete() {
2836 for remove in [
2837 (|cache: &dyn Cache| cache.invalidate("k")) as fn(&dyn Cache),
2838 |cache: &dyn Cache| cache.clear(),
2839 |cache: &dyn Cache| {
2840 let _ = cache.invalidate_namespace("ns");
2841 },
2842 ] {
2843 let cache = with_capsule_seam(Arc::new(PanickingBackend));
2844 let scope = capture_scope();
2845 let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2846 block_on(crate::capsule::capture::with_capture_scope(
2847 Arc::clone(&scope),
2848 async { remove(cache.as_ref()) },
2849 ));
2850 }));
2851 assert!(caught.is_err(), "the backend panics");
2852 let _ = scope.effects_snapshot();
2853 assert!(
2854 scope.is_truncated(),
2855 "an unfilled slot marks the capsule incomplete"
2856 );
2857 }
2858 }
2859
2860 #[cfg(feature = "reporting")]
2864 #[test]
2865 fn a_direct_get_value_is_not_silently_substituted() {
2866 let cache = with_capsule_seam(Arc::new(SpyBackend::default()));
2867 let scope = capture_scope();
2868 block_on(crate::capsule::capture::with_capture_scope(
2869 Arc::clone(&scope),
2870 async {
2871 let _: Option<u32> = get_cached(cache.as_ref(), "k");
2872 },
2873 ));
2874 assert!(!scope.is_truncated(), "a helper read is recorded");
2875 block_on(crate::capsule::capture::with_capture_scope(
2876 Arc::clone(&scope),
2877 async {
2878 let _ = cache.get_value("k");
2879 },
2880 ));
2881 assert!(scope.is_truncated(), "a direct read cannot be recorded");
2882
2883 let tape = Arc::new(crate::capsule::ReplayEffects::new(
2884 crate::capsule::CapsuleEffects::default(),
2885 ));
2886 let read = block_on(crate::capsule::with_effect_tape(Arc::clone(&tape), async {
2887 cache.get_value("k")
2888 }));
2889 assert!(read.is_none());
2890 assert_eq!(tape.divergences().len(), 1, "{:?}", tape.divergences());
2891 }
2892
2893 #[cfg(feature = "reporting")]
2897 #[test]
2898 fn a_direct_write_is_not_silently_dropped() {
2899 let cache = with_capsule_seam(Arc::new(SpyBackend::default()));
2900 let scope = capture_scope();
2901 block_on(crate::capsule::capture::with_capture_scope(
2902 Arc::clone(&scope),
2903 async { insert_cached(cache.as_ref(), "k", 1_u32, None) },
2904 ));
2905 assert!(!scope.is_truncated(), "a helper write is recorded");
2906 for write in [
2907 (|cache: &dyn Cache| cache.insert_value("k", Arc::new(1_u32))) as fn(&dyn Cache),
2908 |cache: &dyn Cache| cache.insert_raw_bytes("k", b"1".to_vec(), None),
2909 ] {
2910 let scope = capture_scope();
2911 block_on(crate::capsule::capture::with_capture_scope(
2912 Arc::clone(&scope),
2913 async { write(cache.as_ref()) },
2914 ));
2915 assert!(scope.is_truncated(), "a direct write cannot be recorded");
2916
2917 let tape = Arc::new(crate::capsule::ReplayEffects::new(
2918 crate::capsule::CapsuleEffects::default(),
2919 ));
2920 block_on(crate::capsule::with_effect_tape(Arc::clone(&tape), async {
2921 write(cache.as_ref());
2922 }));
2923 assert_eq!(tape.divergences().len(), 1, "{:?}", tape.divergences());
2924 }
2925 }
2926
2927 #[cfg(feature = "reporting")]
2929 #[test]
2930 fn the_capsule_seam_wraps_a_backend_once() {
2931 let spy: Arc<dyn Cache> = Arc::new(SpyBackend::default());
2932 let once = with_capsule_seam(spy);
2933 let twice = with_capsule_seam(Arc::clone(&once));
2934 assert!(Arc::ptr_eq(&once, &twice));
2935 }
2936
2937 #[test]
2938 fn invalidation_error_reports_attempts_and_reason() {
2939 let err = InvalidationError::new(3, "READONLY");
2940 assert_eq!(err.attempts(), 3);
2941 assert_eq!(err.reason(), "READONLY");
2942 assert_eq!(
2943 err.to_string(),
2944 "cache invalidation failed after 3 attempt(s): READONLY"
2945 );
2946 }
2947
2948 #[test]
2949 fn invalidation_failures_counter_is_monotonic() {
2950 let before = invalidation_failures_total();
2951 record_invalidation_failure();
2952 assert!(invalidation_failures_total() > before);
2953 }
2954
2955 #[test]
2956 fn cacheable_result_ok_round_trips() {
2957 let r: Result<i32, &str> = Result::from_ok(42);
2958 assert_eq!(r, Ok(42));
2959 assert_eq!(r.into_result(), Ok(42));
2960 }
2961
2962 #[test]
2963 fn cacheable_result_err_passes_through() {
2964 let r: Result<i32, &str> = Err("oops");
2965 assert_eq!(r.into_result(), Err("oops"));
2966 }
2967}