Skip to main content

chronon_runtime/
builder.rs

1//! [`ChrononBuilder`] and [`DeploymentShape`] — runtime configuration and assembly.
2
3use std::sync::Arc;
4
5use chronon_core::context::{ContextFactory, NoOpContextFactory};
6use chronon_core::error::{ChrononError, Result};
7use chronon_core::store::SchedulerStore;
8use chronon_executor::{Executor, ScriptRegistry};
9use chronon_scheduler::{Scheduler, SchedulerConfig};
10use chronon_telemetry::{NoOpSink, TelemetrySink};
11use tokio::sync::{mpsc, Notify};
12
13/// Named deployment assembly — not a global mode enum.
14///
15/// Selected via [`ChrononBuilder`] fluent methods; drives which loops [`crate::Chronon::run`] starts.
16#[derive(Debug, Clone, PartialEq, Eq, Default)]
17pub enum DeploymentShape {
18    /// Coordinator tick loop + worker in one process (default).
19    #[default]
20    Embedded,
21    /// Scheduler tick and partition assigner only; no script execution.
22    CoordinatorOnly,
23    /// Worker loop for `pool_id`; claims runs from the shared store.
24    Worker(String),
25    /// No local loops; host uses [`crate::RemoteCoordinatorClient`] against `base_url`.
26    RemoteClient(String),
27}
28
29/// Builds a [`crate::Chronon`] runtime with explicit adapter injection.
30///
31/// Hosts call fluent setters then [`Self::build`]. Missing store is the only hard requirement;
32/// context factory, telemetry, and registry fall back to no-op defaults.
33///
34/// # Examples
35///
36/// ```
37/// use std::sync::Arc;
38/// use chronon_backend_mem::InMemorySchedulerStore;
39/// use chronon_runtime::ChrononBuilder;
40///
41/// let store = Arc::new(InMemorySchedulerStore::new());
42/// let chronon = ChrononBuilder::new()
43///     .scheduler_store(store)
44///     .embedded()
45///     .build()
46///     .unwrap();
47/// assert_eq!(chronon.executor().script_count(), 0);
48/// ```
49pub struct ChrononBuilder {
50    store: Option<Arc<dyn SchedulerStore>>,
51    context_factory: Option<Arc<dyn ContextFactory>>,
52    telemetry: Option<Arc<dyn TelemetrySink>>,
53    registry: Option<Arc<ScriptRegistry>>,
54    deployment: DeploymentShape,
55    auto_registry: bool,
56    tick_interval_ms: u64,
57    instance_id: Option<String>,
58}
59
60impl ChrononBuilder {
61    /// Empty builder: embedded deployment, env-default tick interval, no store.
62    pub fn new() -> Self {
63        Self {
64            store: None,
65            context_factory: None,
66            telemetry: None,
67            registry: None,
68            deployment: DeploymentShape::Embedded,
69            auto_registry: false,
70            tick_interval_ms: chronon_scheduler::tick_interval_ms_from_env(),
71            instance_id: None,
72        }
73    }
74
75    /// Required unless [`Self::scheduler_store_from_global`] is used.
76    pub fn scheduler_store(mut self, store: Arc<dyn SchedulerStore>) -> Self {
77        self.store = Some(store);
78        self
79    }
80
81    /// Installs the process-global default store (e.g. mem backend); errors if unset.
82    pub fn scheduler_store_from_global(mut self) -> Result<Self> {
83        self.store = Some(chronon_core::default_store_from_global()?);
84        Ok(self)
85    }
86
87    /// Factory used when executing scripts; defaults to [`NoOpContextFactory`].
88    pub fn context_factory(mut self, factory: Arc<dyn ContextFactory>) -> Self {
89        self.context_factory = Some(factory);
90        self
91    }
92
93    /// Metrics sink shared by scheduler and executor; defaults to [`NoOpSink`].
94    pub fn telemetry_sink(mut self, sink: Arc<dyn TelemetrySink>) -> Self {
95        self.telemetry = Some(sink);
96        self
97    }
98
99    /// Script registry for the executor; use [`Self::auto_registry`] to populate from inventory.
100    pub fn script_registry(mut self, registry: Arc<ScriptRegistry>) -> Self {
101        self.registry = Some(registry);
102        self
103    }
104
105    /// Stable id for scheduler leader election and worker rows; random UUID if omitted.
106    pub fn instance_id(mut self, id: impl Into<String>) -> Self {
107        self.instance_id = Some(id.into());
108        self
109    }
110
111    /// Embedded coordinator + worker loops in one process.
112    pub fn embedded(mut self) -> Self {
113        self.deployment = DeploymentShape::Embedded;
114        self
115    }
116
117    /// Coordinator-only: tick and partition assigner, no worker slots.
118    pub fn coordinator_only(mut self) -> Self {
119        self.deployment = DeploymentShape::CoordinatorOnly;
120        self
121    }
122
123    /// Worker-only: claim and execute runs for `pool_id`.
124    pub fn worker(mut self, pool_id: impl Into<String>) -> Self {
125        self.deployment = DeploymentShape::Worker(pool_id.into());
126        self
127    }
128
129    /// Remote client shape: no local loops; pair with [`crate::RemoteCoordinatorClient`].
130    pub fn remote_coordinator(mut self, base_url: impl Into<String>) -> Self {
131        self.deployment = DeploymentShape::RemoteClient(base_url.into());
132        self
133    }
134
135    /// Populate registry from `inventory` (`#[chronon::script]` link-time registration).
136    pub fn auto_registry(mut self) -> Self {
137        self.auto_registry = true;
138        self
139    }
140
141    /// Scheduler tick period in milliseconds; overrides `CHRONON_TICK_INTERVAL_MS` when set.
142    pub fn tick_interval_ms(mut self, ms: u64) -> Self {
143        self.tick_interval_ms = ms;
144        self
145    }
146
147    /// Assemble [`crate::Chronon`]; returns [`ChrononError::Internal`] if store was not configured.
148    pub fn build(self) -> Result<super::Chronon> {
149        let store = self
150            .store
151            .ok_or_else(|| ChrononError::Internal("scheduler_store is required".into()))?;
152        let context_factory = self
153            .context_factory
154            .unwrap_or_else(|| Arc::new(NoOpContextFactory));
155        let telemetry = self
156            .telemetry
157            .unwrap_or_else(|| Arc::new(NoOpSink) as Arc<dyn TelemetrySink>);
158        let registry = match self.registry {
159            Some(registry) => registry,
160            None if self.auto_registry => Arc::new(ScriptRegistry::from_inventory()),
161            None => Arc::new(ScriptRegistry::new()),
162        };
163
164        let embedded_partitions = matches!(self.deployment, DeploymentShape::Embedded);
165        let instance_id = self
166            .instance_id
167            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
168
169        let scheduler = Arc::new(Scheduler::new(
170            SchedulerConfig {
171                tick_interval_ms: self.tick_interval_ms,
172                instance_id,
173                num_partitions: chronon_scheduler::num_partitions_from_env(),
174                embedded: embedded_partitions,
175            },
176            store.clone(),
177            telemetry.clone(),
178        ));
179
180        let (event_tx, event_rx) = mpsc::unbounded_channel();
181        let executor = Arc::new(Executor::new(
182            registry,
183            context_factory,
184            telemetry,
185            event_tx,
186        ));
187
188        Ok(super::Chronon::new(
189            store,
190            scheduler,
191            executor,
192            self.deployment,
193            Arc::new(Notify::new()),
194            event_rx,
195        ))
196    }
197}
198
199impl Default for ChrononBuilder {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205/// Shorthand for [`ChrononBuilder::new`].
206pub fn builder() -> ChrononBuilder {
207    ChrononBuilder::new()
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use chronon_backend_mem::InMemorySchedulerStore;
214    use chronon_telemetry::ConsoleSink;
215
216    #[tokio::test]
217    async fn builder_embedded_compiles() {
218        let store = Arc::new(InMemorySchedulerStore::new());
219        let chronon = ChrononBuilder::new()
220            .scheduler_store(store)
221            .telemetry_sink(Arc::new(ConsoleSink))
222            .embedded()
223            .build()
224            .expect("build");
225        assert_eq!(chronon.deployment, DeploymentShape::Embedded);
226    }
227
228    #[tokio::test]
229    async fn builder_auto_registry_from_inventory() {
230        let store = Arc::new(InMemorySchedulerStore::new());
231        let chronon = ChrononBuilder::new()
232            .scheduler_store(store)
233            .embedded()
234            .auto_registry()
235            .build()
236            .expect("build");
237        let _ = chronon.executor().script_count();
238    }
239
240    #[tokio::test]
241    async fn builder_scheduler_store_from_global() {
242        let _installed = chronon_backend_mem::install_default_mem_store();
243        let chronon = ChrononBuilder::new()
244            .scheduler_store_from_global()
245            .expect("global store")
246            .embedded()
247            .build()
248            .expect("build");
249        assert_eq!(chronon.deployment, DeploymentShape::Embedded);
250    }
251}