1use std::collections::{HashMap, HashSet};
4use std::future::Future;
5use std::num::NonZeroUsize;
6use std::panic::{catch_unwind, AssertUnwindSafe};
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10use futures::future::BoxFuture;
11use parking_lot::Mutex;
12
13use crate::clock::Clock;
14use crate::error::{BoxError, ConfigError, Error};
15use crate::flight::{start_pending, Flight, Settled, ValueResult};
16use crate::identity::{Identity, IntoKeyId};
17use crate::limits::{
18 DEFAULT_LOCAL_CAPACITY, DEFAULT_REMOTE_READ_TIMEOUT_MS, DEFAULT_SHADOW_MAX_IN_FLIGHT,
19 MAX_DEADLINE_MS, MAX_SAFE_INTEGER, MAX_SUPPORTED_DURATION_MS, WATERMARK_USE_CASE,
20};
21use crate::local::{LocalEntry, LocalRead, LocalStore, LruLocalStore, StoredValue};
22use crate::observe::{Event, Labels, Layer, LogEvent, Logger, Observer, OutcomeLabels};
23use crate::operation::{downcast_value, erase_load, ErasedOperation, Operation, RecoveryPredicate};
24use crate::policy::RuntimePolicy;
25use crate::protocol::CompressionConfig;
26use crate::remote::{InvalidateRequest, Remote};
27use crate::runtime::Runtime;
28use crate::scope::{Owner, Scope};
29use crate::shadow::ShadowFlight;
30
31pub type PolicyProvider = Arc<
33 dyn Fn(Identity) -> BoxFuture<'static, Result<Option<RuntimePolicy>, BoxError>> + Send + Sync,
34>;
35
36static NEXT_CACHE_ID: AtomicU64 = AtomicU64::new(1);
37
38pub(crate) struct CoreState {
39 pub(crate) local: Option<Box<dyn LocalStore>>,
40 pub(crate) flights: HashMap<String, Arc<Flight>>,
41 pub(crate) shadows: HashMap<String, Arc<ShadowFlight>>,
42 pub(crate) registered: HashSet<String>,
43}
44
45pub(crate) struct Core {
46 pub(crate) id: u64,
47 pub(crate) namespace: Arc<str>,
48 pub(crate) remote: Option<Arc<dyn Remote>>,
49 pub(crate) observer: Option<Arc<dyn Observer>>,
50 pub(crate) logger: Arc<dyn Logger>,
51 pub(crate) provider: Option<PolicyProvider>,
52 pub(crate) remote_read_timeout_ms: u64,
53 pub(crate) shadow_max_in_flight: usize,
54 pub(crate) should_recover: Option<RecoveryPredicate>,
55 pub(crate) compression: Option<CompressionConfig>,
57 pub(crate) clock: Arc<dyn Clock>,
58 pub(crate) runtime: Arc<dyn Runtime>,
59 pub(crate) state: Mutex<CoreState>,
60}
61
62impl Core {
63 pub(crate) fn emit(&self, event: Event) {
65 if let Some(observer) = &self.observer {
66 let _ = catch_unwind(AssertUnwindSafe(|| observer.observe(&event)));
67 }
68 }
69
70 pub(crate) fn log(&self, event: LogEvent) -> bool {
73 catch_unwind(AssertUnwindSafe(|| self.logger.log(&event))).is_ok()
74 }
75
76 pub(crate) fn shadow_hook_enabled(&self) -> bool {
77 match &self.observer {
78 Some(observer) => {
79 catch_unwind(AssertUnwindSafe(|| observer.observes_shadow_outcomes()))
80 .unwrap_or(false)
81 }
82 None => false,
83 }
84 }
85
86 pub(crate) fn local_get(&self, key: &str) -> Result<Option<StoredValue>, BoxError> {
88 let now_ms = self.clock.elapsed_ms();
91 let read = {
92 let mut state = self.state.lock();
93 match state.local.as_mut() {
94 None => return Ok(None),
95 Some(local) => catch_unwind(AssertUnwindSafe(|| local.get(key, now_ms)))
96 .unwrap_or_else(|payload| {
97 Err(crate::flight::panic_message(payload).to_string().into())
98 }),
99 }
100 };
101 match read? {
104 LocalRead::Live(value) => Ok(Some(value)),
105 LocalRead::Absent | LocalRead::Expired(_) => Ok(None),
106 }
107 }
108
109 pub(crate) fn local_put(
110 &self,
111 key: &str,
112 value: StoredValue,
113 ttl_ms: u64,
114 ) -> Result<(), BoxError> {
115 let now_ms = self.clock.elapsed_ms();
116 let displaced = {
117 let mut state = self.state.lock();
118 match state.local.as_mut() {
119 None => return Ok(()),
120 Some(local) => {
121 let entry = LocalEntry {
122 value,
123 inserted_ms: now_ms,
124 ttl_ms: ttl_ms.min(i64::MAX as u64) as i64,
125 };
126 catch_unwind(AssertUnwindSafe(|| local.put(key.to_string(), entry)))
127 .unwrap_or_else(|payload| {
128 Err(crate::flight::panic_message(payload).to_string().into())
129 })
130 }
131 }
132 };
133 displaced.map(|_| ())
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Default)]
140pub struct ProcessCoalescingState {
141 pub active_leaders: usize,
143 pub active_followers: usize,
145 pub oldest_leader_age_ms: Option<u64>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Default)]
152pub struct CoalescingState {
153 pub process: ProcessCoalescingState,
155}
156
157pub struct DialCacheBuilder {
159 namespace: String,
160 remote: Option<Arc<dyn Remote>>,
161 observer: Option<Arc<dyn Observer>>,
162 logger: Option<Arc<dyn Logger>>,
163 provider: Option<PolicyProvider>,
164 local_capacity: Option<usize>,
165 local_store: Option<Box<dyn LocalStore>>,
166 remote_read_timeout_ms: Option<u64>,
167 shadow_max_in_flight: Option<usize>,
168 should_recover: Option<RecoveryPredicate>,
169 compression: Option<Option<CompressionConfig>>,
170 clock: Option<Arc<dyn Clock>>,
171 runtime: Option<Arc<dyn Runtime>>,
172}
173
174impl std::fmt::Debug for DialCacheBuilder {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("DialCacheBuilder")
177 .field("namespace", &self.namespace)
178 .finish_non_exhaustive()
179 }
180}
181
182impl DialCacheBuilder {
183 fn new() -> Self {
184 DialCacheBuilder {
185 namespace: "urn".to_string(),
186 remote: None,
187 observer: None,
188 logger: None,
189 provider: None,
190 local_capacity: None,
191 local_store: None,
192 remote_read_timeout_ms: None,
193 shadow_max_in_flight: None,
194 should_recover: None,
195 compression: None,
196 clock: None,
197 runtime: None,
198 }
199 }
200
201 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
204 self.namespace = namespace.into();
205 self
206 }
207
208 pub fn remote(mut self, remote: impl Remote) -> Self {
210 self.remote = Some(Arc::new(remote));
211 self
212 }
213
214 pub fn remote_arc(mut self, remote: Arc<dyn Remote>) -> Self {
216 self.remote = Some(remote);
217 self
218 }
219
220 pub fn observer(mut self, observer: impl Observer) -> Self {
222 self.observer = Some(Arc::new(observer));
223 self
224 }
225
226 pub fn observer_arc(mut self, observer: Arc<dyn Observer>) -> Self {
228 self.observer = Some(observer);
229 self
230 }
231
232 pub fn logger(mut self, logger: impl Logger) -> Self {
235 self.logger = Some(Arc::new(logger));
236 self
237 }
238
239 pub fn logger_arc(mut self, logger: Arc<dyn Logger>) -> Self {
241 self.logger = Some(logger);
242 self
243 }
244
245 pub fn policy_provider<F, Fut>(mut self, provider: F) -> Self
248 where
249 F: Fn(Identity) -> Fut + Send + Sync + 'static,
250 Fut: Future<Output = Result<Option<RuntimePolicy>, BoxError>> + Send + 'static,
251 {
252 self.provider = Some(Arc::new(move |identity| Box::pin(provider(identity))));
253 self
254 }
255
256 pub fn local_capacity(mut self, capacity: usize) -> Self {
259 self.local_capacity = Some(capacity);
260 self
261 }
262
263 pub fn local_store(mut self, store: Box<dyn LocalStore>) -> Self {
266 self.local_store = Some(store);
267 self
268 }
269
270 pub fn remote_read_timeout_ms(mut self, ms: u64) -> Self {
272 self.remote_read_timeout_ms = Some(ms);
273 self
274 }
275
276 pub fn shadow_max_in_flight(mut self, jobs: usize) -> Self {
278 self.shadow_max_in_flight = Some(jobs);
279 self
280 }
281
282 pub fn should_recover(
285 mut self,
286 predicate: impl Fn(&Error) -> bool + Send + Sync + 'static,
287 ) -> Self {
288 self.should_recover = Some(Arc::new(predicate));
289 self
290 }
291
292 pub fn compression(mut self, config: CompressionConfig) -> Self {
294 self.compression = Some(Some(config));
295 self
296 }
297
298 pub fn disable_compression(mut self) -> Self {
300 self.compression = Some(None);
301 self
302 }
303
304 pub fn clock(mut self, clock: impl Clock) -> Self {
307 self.clock = Some(Arc::new(clock));
308 self
309 }
310
311 pub fn clock_arc(mut self, clock: Arc<dyn Clock>) -> Self {
313 self.clock = Some(clock);
314 self
315 }
316
317 pub fn runtime_arc(mut self, runtime: Arc<dyn Runtime>) -> Self {
319 self.runtime = Some(runtime);
320 self
321 }
322
323 pub fn runtime(mut self, runtime: impl Runtime) -> Self {
325 self.runtime = Some(Arc::new(runtime));
326 self
327 }
328
329 pub fn build(self) -> Result<DialCache, ConfigError> {
336 if self.namespace.contains(['{', '}']) {
337 return Err(ConfigError::invalid(
338 "DialCache namespace must not contain \"{\" or \"}\"",
339 ));
340 }
341 let compression = match self.compression {
342 None => Some(CompressionConfig::default()),
343 Some(None) => None,
344 Some(Some(config)) => {
345 config
346 .validate()
347 .map_err(|e| ConfigError::invalid(e.to_string()))?;
348 Some(config)
349 }
350 };
351 let remote_read_timeout_ms = self
352 .remote_read_timeout_ms
353 .unwrap_or(DEFAULT_REMOTE_READ_TIMEOUT_MS);
354 if !(1..=MAX_DEADLINE_MS).contains(&remote_read_timeout_ms) {
355 return Err(ConfigError::invalid(format!(
356 "DialCache remote read timeout must be a positive integer no greater than {MAX_DEADLINE_MS} ms"
357 )));
358 }
359 let shadow_max_in_flight = self
360 .shadow_max_in_flight
361 .unwrap_or(DEFAULT_SHADOW_MAX_IN_FLIGHT);
362 if shadow_max_in_flight == 0 {
363 return Err(ConfigError::invalid(
364 "DialCache shadow_max_in_flight must be positive",
365 ));
366 }
367 let local: Option<Box<dyn LocalStore>> = match self.local_store {
368 Some(store) => Some(store),
369 None => {
370 let capacity = self.local_capacity.unwrap_or(DEFAULT_LOCAL_CAPACITY);
371 if capacity as u64 > MAX_SAFE_INTEGER {
372 return Err(ConfigError::invalid(
373 "DialCache local capacity must be a safe integer",
374 ));
375 }
376 NonZeroUsize::new(capacity)
377 .map(|capacity| Box::new(LruLocalStore::new(capacity)) as Box<dyn LocalStore>)
378 }
379 };
380 let clock = match self.clock {
381 Some(clock) => clock,
382 None => default_clock()?,
383 };
384 let runtime = match self.runtime {
385 Some(runtime) => runtime,
386 None => default_runtime()?,
387 };
388 let core = Core {
389 id: NEXT_CACHE_ID.fetch_add(1, Ordering::Relaxed),
390 namespace: Arc::from(self.namespace.as_str()),
391 remote: self.remote,
392 observer: self.observer,
393 logger: self
394 .logger
395 .unwrap_or_else(|| Arc::new(crate::observe::LogFacadeLogger)),
396 provider: self.provider,
397 remote_read_timeout_ms,
398 shadow_max_in_flight,
399 should_recover: self.should_recover,
400 compression,
401 clock,
402 runtime,
403 state: Mutex::new(CoreState {
404 local,
405 flights: HashMap::new(),
406 shadows: HashMap::new(),
407 registered: HashSet::new(),
408 }),
409 };
410 Ok(DialCache {
411 core: Arc::new(core),
412 })
413 }
414}
415
416fn default_clock() -> Result<Arc<dyn Clock>, ConfigError> {
417 Ok(Arc::new(crate::clock::SystemClock::new()))
418}
419
420#[cfg(feature = "tokio")]
421fn default_runtime() -> Result<Arc<dyn Runtime>, ConfigError> {
422 Ok(Arc::new(crate::runtime::TokioRuntime::current()?))
423}
424
425#[cfg(not(feature = "tokio"))]
426fn default_runtime() -> Result<Arc<dyn Runtime>, ConfigError> {
427 Err(ConfigError::invalid("DialCache needs a runtime: enable the tokio feature or supply one with DialCacheBuilder::runtime"))
428}
429
430#[derive(Debug)]
432pub struct ScopeGuard {
433 scope: Scope,
434 owner: Arc<Owner>,
435}
436
437impl ScopeGuard {
438 pub fn scope(&self) -> &Scope {
440 &self.scope
441 }
442}
443
444impl Drop for ScopeGuard {
445 fn drop(&mut self) {
446 self.owner.close();
447 }
448}
449
450#[derive(Clone)]
454pub struct DialCache {
455 pub(crate) core: Arc<Core>,
456}
457
458impl std::fmt::Debug for DialCache {
459 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460 f.debug_struct("DialCache")
461 .field("namespace", &self.core.namespace)
462 .finish_non_exhaustive()
463 }
464}
465
466impl DialCache {
467 pub fn builder() -> DialCacheBuilder {
470 DialCacheBuilder::new()
471 }
472
473 pub fn namespace(&self) -> &str {
475 &self.core.namespace
476 }
477
478 pub fn enable_guard(&self) -> ScopeGuard {
486 let owner = Owner::new();
487 let scope = Scope {
488 cache_id: self.core.id,
489 owner: Some(owner.clone()),
490 enabled: true,
491 };
492 ScopeGuard { scope, owner }
493 }
494
495 pub async fn enable<F, Fut, R>(&self, f: F) -> R
499 where
500 F: FnOnce(Scope) -> Fut,
501 Fut: Future<Output = R>,
502 {
503 let guard = self.enable_guard();
504 let scope = guard.scope().clone();
505 let result = f(scope).await;
506 drop(guard);
507 result
508 }
509
510 pub async fn enable_in<F, Fut, R>(&self, parent: &Scope, f: F) -> R
513 where
514 F: FnOnce(Scope) -> Fut,
515 Fut: Future<Output = R>,
516 {
517 let live = if parent.cache_id == self.core.id {
518 parent.live_owner()
519 } else {
520 None
521 };
522 match live {
523 Some(owner) => {
524 let scope = Scope {
525 cache_id: self.core.id,
526 owner: Some(owner),
527 enabled: true,
528 };
529 f(scope).await
530 }
531 None => self.enable(f).await,
532 }
533 }
534
535 pub async fn disable_in<F, Fut, R>(&self, parent: &Scope, f: F) -> R
538 where
539 F: FnOnce(Scope) -> Fut,
540 Fut: Future<Output = R>,
541 {
542 let owner = if parent.cache_id == self.core.id {
543 parent.owner.clone()
544 } else {
545 None
546 };
547 let scope = Scope {
548 cache_id: self.core.id,
549 owner,
550 enabled: false,
551 };
552 f(scope).await
553 }
554
555 pub fn is_enabled(&self, scope: &Scope) -> bool {
557 scope.cache_id == self.core.id && scope.is_enabled()
558 }
559
560 pub fn coalescing_state(&self) -> CoalescingState {
562 let now = self.core.clock.elapsed();
563 let state = self.core.state.lock();
564 let mut process = ProcessCoalescingState {
565 active_leaders: state.flights.len(),
566 ..Default::default()
567 };
568 for flight in state.flights.values() {
569 process.active_followers += flight.followers();
570 let age = now
571 .saturating_sub(flight.started)
572 .as_millis()
573 .min(u64::MAX as u128) as u64;
574 if process
575 .oldest_leader_age_ms
576 .is_none_or(|oldest| age > oldest)
577 {
578 process.oldest_leader_age_ms = Some(age);
579 }
580 }
581 CoalescingState { process }
582 }
583
584 pub async fn invalidate(
589 &self,
590 key_type: &str,
591 id: impl IntoKeyId,
592 future_buffer_ms: u64,
593 ) -> Result<(), Error> {
594 let identity = Identity::new(key_type, id.into_key_id(), WATERMARK_USE_CASE)
595 .tracked(true)
596 .namespace(self.core.namespace.to_string());
597 self.invalidate_identity(identity, future_buffer_ms).await
598 }
599
600 pub async fn invalidate_identity(
609 &self,
610 mut identity: Identity,
611 future_buffer_ms: u64,
612 ) -> Result<(), Error> {
613 let core = &self.core;
614 if future_buffer_ms > MAX_SUPPORTED_DURATION_MS {
615 return Err(ConfigError::invalid(
616 "DialCache invalidation future buffer must be no greater than 365 days",
617 )
618 .into());
619 }
620 if identity.namespace.is_empty() {
621 identity.namespace = core.namespace.to_string();
622 }
623 identity.tracked = true;
624 let namespace: Arc<str> = Arc::from(identity.namespace.as_str());
625 let key_type: Arc<str> = Arc::from(identity.key_type.as_str());
626 core.emit(Event::Invalidation {
627 namespace: namespace.clone(),
628 key_type: key_type.clone(),
629 layer: Layer::Remote,
630 });
631 let result: Result<(), Error> = async {
632 let remote = core.remote.clone().ok_or(Error::MissingRemote)?;
633 let keys = identity
634 .keys()
635 .map_err(|e| Error::Config(ConfigError::invalid(e.to_string())))?;
636 let watermark_key = keys.watermark.ok_or_else(|| {
637 Error::Config(ConfigError::invalid(
638 "tracked identity has no watermark key",
639 ))
640 })?;
641 let now = core.clock.wall_ms();
642 if now < 0 || now as u64 > MAX_SAFE_INTEGER - future_buffer_ms {
643 return Err(ConfigError::invalid(
644 "DialCache invalidation timestamp is outside the safe integer domain",
645 )
646 .into());
647 }
648 let request = InvalidateRequest {
649 watermark_key,
650 invalidated_at_ms: now as u64,
651 future_buffer_ms,
652 };
653 let pending: Settled<Result<(), Error>> = start_pending(
654 core.runtime.as_ref(),
655 async move {
656 remote
657 .invalidate(request)
658 .await
659 .map_err(|e| Error::Remote(Arc::from(e)))
660 },
661 |message| Err(Error::Panic(message)),
662 );
663 pending.wait().await
664 }
665 .await;
666 if let Err(error) = &result {
667 core.log(LogEvent::InvalidationFailed(error.to_string().into()));
668 core.emit(Event::Error {
669 labels: Labels {
670 namespace,
671 use_case: Arc::from(WATERMARK_USE_CASE),
672 key_type,
673 layer: Layer::Remote,
674 },
675 error: crate::observe::ErrorKind::Invalidation,
676 in_fallback: false,
677 });
678 }
679 result
680 }
681
682 pub async fn get_or_load<T, F, Fut>(
688 &self,
689 scope: &Scope,
690 operation: Operation<T>,
691 load: F,
692 ) -> Result<Arc<T>, Error>
693 where
694 T: Send + Sync + 'static,
695 F: Fn(Scope) -> Fut + Send + Sync + 'static,
696 Fut: Future<Output = Result<T, BoxError>> + Send + 'static,
697 {
698 let (identity, metadata) = operation.erase();
699 let erased = ErasedOperation {
700 identity,
701 identity_provider: None,
702 metadata,
703 load: erase_load(load),
704 };
705 let value = self.execute(scope, erased).await?;
706 downcast_value::<T>(value)
707 }
708
709 pub(crate) async fn execute(
714 &self,
715 scope: &Scope,
716 mut operation: ErasedOperation,
717 ) -> ValueResult {
718 validate_operation(&operation)?;
719 if operation.identity.namespace.is_empty() {
720 operation.identity.namespace = self.core.namespace.to_string();
721 }
722 let core = self.core.clone();
725 let scope = scope.clone();
726 let pending: Settled<ValueResult> = start_pending(
727 core.runtime.clone().as_ref(),
728 crate::execution::run(core, scope, Arc::new(operation)),
729 |message| Err(Error::Panic(message)),
730 );
731 pending.wait().await
732 }
733
734 pub(crate) fn register_use_case(&self, use_case: &str) -> Result<(), ConfigError> {
735 if use_case == WATERMARK_USE_CASE {
736 return Err(ConfigError::ReservedUseCase(use_case.to_string()));
737 }
738 let mut state = self.core.state.lock();
739 if !state.registered.insert(use_case.to_string()) {
740 return Err(ConfigError::UseCaseAlreadyRegistered(use_case.to_string()));
741 }
742 Ok(())
743 }
744}
745
746pub(crate) fn validate_operation(operation: &ErasedOperation) -> Result<(), Error> {
747 operation
748 .metadata
749 .policy
750 .validate()
751 .map_err(|e| ConfigError::invalid(e.to_string()))?;
752 if let crate::operation::SourceBudget::Millis(ms) = operation.metadata.budget {
753 if !(1..=MAX_DEADLINE_MS).contains(&ms) {
754 return Err(ConfigError::invalid(format!(
755 "DialCache source budget must be a positive integer no greater than {MAX_DEADLINE_MS} ms"
756 ))
757 .into());
758 }
759 }
760 if operation.identity.use_case == WATERMARK_USE_CASE {
761 return Err(ConfigError::ReservedUseCase(operation.identity.use_case.clone()).into());
762 }
763 Ok(())
764}
765
766pub(crate) fn outcome_labels(identity: &Identity) -> OutcomeLabels {
767 OutcomeLabels {
768 namespace: Arc::from(identity.namespace.as_str()),
769 use_case: Arc::from(identity.use_case.as_str()),
770 key_type: Arc::from(identity.key_type.as_str()),
771 }
772}