Skip to main content

chronon_core/
router.rs

1//! Named [`SchedulerStore`] registration at host boot.
2//!
3//! Register one or more backends under logical names before constructing
4//! `Chronon` in `chronon-runtime`. Use [`StoreRouter::register_global`] with
5//! [`DEFAULT_STORE_NAME`] for single-store setups.
6
7use std::collections::HashMap;
8use std::sync::{Arc, OnceLock};
9
10use parking_lot::RwLock;
11
12use crate::error::{ChrononError, Result};
13use crate::store::SchedulerStore;
14
15/// Default logical store name when hosts register a single backend.
16pub const DEFAULT_STORE_NAME: &str = "default";
17
18static GLOBAL_ROUTER: OnceLock<RwLock<StoreRouter>> = OnceLock::new();
19
20fn global_router() -> &'static RwLock<StoreRouter> {
21    GLOBAL_ROUTER.get_or_init(|| RwLock::new(StoreRouter::new()))
22}
23
24/// Registers named [`SchedulerStore`] backends at host boot.
25///
26/// Use for multi-store hosts or a single default via [`DEFAULT_STORE_NAME`]. Typical embedded
27/// flow:
28///
29/// 1. [`Self::register_global`] (or `install_default_mem_store` from the mem backend).
30/// 2. `ChrononBuilder::scheduler_store_from_global()`.
31///
32/// Prefer passing a store directly to `ChrononBuilder::scheduler_store` in coordinator–worker
33/// or remote-HTTP setups when each binary already shares connection URLs — the global router
34/// is optional convenience for single-process boots. Multi-process and remote setups still
35/// require a shared durable database.
36///
37/// Thread-safe when accessed through [`Self::register_global`] / [`default_store_from_global`];
38/// direct mutation requires exclusive access to the router instance.
39///
40/// # Examples
41///
42/// ```
43/// # use std::sync::Arc;
44/// # use chronon_core::{SchedulerStore, StoreRouter, DEFAULT_STORE_NAME};
45/// # fn demo(store: Arc<dyn SchedulerStore>) {
46/// StoreRouter::register_global(DEFAULT_STORE_NAME, store);
47/// # }
48/// ```
49///
50/// Runnable end-to-end: `cargo run -p uf-chronon --example store_router_boot --features mem`.
51#[derive(Default)]
52pub struct StoreRouter {
53    stores: HashMap<String, Arc<dyn SchedulerStore>>,
54}
55
56impl StoreRouter {
57    /// Create an empty router (no stores registered).
58    pub fn new() -> Self {
59        Self {
60            stores: HashMap::new(),
61        }
62    }
63
64    /// Register a store under a logical name (overwrites any previous entry).
65    pub fn register(&mut self, name: impl Into<String>, store: Arc<dyn SchedulerStore>) {
66        self.stores.insert(name.into(), store);
67    }
68
69    /// Resolve a registered store by name.
70    pub fn get(&self, name: &str) -> Option<Arc<dyn SchedulerStore>> {
71        self.stores.get(name).cloned()
72    }
73
74    /// Replace the process-global router (typically once at startup).
75    ///
76    /// Subsequent calls are ignored; the first successful install wins.
77    pub fn install_global(router: Self) {
78        let _ = GLOBAL_ROUTER.set(RwLock::new(router));
79    }
80
81    /// Register a store on the process-global router.
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// # use std::sync::Arc;
87    /// # use chronon_core::{SchedulerStore, StoreRouter, DEFAULT_STORE_NAME};
88    /// # fn demo(store: Arc<dyn SchedulerStore>) {
89    /// StoreRouter::register_global(DEFAULT_STORE_NAME, store);
90    /// # }
91    /// ```
92    pub fn register_global(name: impl Into<String>, store: Arc<dyn SchedulerStore>) {
93        global_router().write().register(name, store);
94    }
95}
96
97/// Resolves the default store from the global router.
98///
99/// Returns [`ChrononError::StorageError`] when no store is registered under
100/// [`DEFAULT_STORE_NAME`].
101pub fn default_store_from_global() -> Result<Arc<dyn SchedulerStore>> {
102    global_router()
103        .read()
104        .get(DEFAULT_STORE_NAME)
105        .ok_or_else(|| ChrononError::storage("no default SchedulerStore registered"))
106}