Skip to main content

chronon_executor/
registry.rs

1//! In-memory [`ScriptRegistry`] with optional link-time inventory discovery.
2
3use std::collections::HashMap;
4
5use chronon_core::{ChrononError, Result};
6
7use crate::descriptor::{InvokeFn, ScriptDescriptor};
8
9struct StoredScript {
10    name: String,
11    invoke: InvokeFn,
12    signature_json: String,
13    signature_hash: u64,
14}
15
16/// In-memory script registry with optional link-time inventory discovery.
17///
18/// Populated at boot via [`Self::from_inventory`] / [`Self::register_from_inventory`] (from
19/// `#[chronon::script]`) or manual [`Self::register`] calls. Read by the executor when a run is
20/// claimed.
21///
22/// In Mode 2 (coordinator + worker), scripts must be registered on **worker** binaries —
23/// that is where handlers execute. Prefer `ChrononBuilder::auto_registry()` so inventory is
24/// collected automatically.
25///
26/// # Examples
27///
28/// Empty registry:
29///
30/// ```
31/// use chronon_executor::ScriptRegistry;
32///
33/// let registry = ScriptRegistry::new();
34/// assert!(registry.is_empty());
35/// ```
36///
37/// Explicit register (daemon-style, without the macro):
38///
39/// ```
40/// use chronon_core::{Result, ScriptContext};
41/// use chronon_executor::{ScriptDescriptor, ScriptRegistry};
42///
43/// fn noop(
44///     _ctx: Box<dyn ScriptContext>,
45///     _params: serde_json::Value,
46/// ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>> {
47///     Box::pin(async { Ok(()) })
48/// }
49///
50/// let mut registry = ScriptRegistry::new();
51/// registry.register(ScriptDescriptor::new("daemon-noop", noop));
52/// assert!(registry.contains("daemon-noop"));
53/// assert_eq!(registry.len(), 1);
54/// ```
55pub struct ScriptRegistry {
56    scripts: HashMap<String, StoredScript>,
57}
58
59impl Default for ScriptRegistry {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl ScriptRegistry {
66    /// Creates an empty registry with no registered scripts.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use chronon_executor::ScriptRegistry;
72    ///
73    /// let registry = ScriptRegistry::new();
74    /// assert!(registry.is_empty());
75    /// ```
76    pub fn new() -> Self {
77        Self {
78            scripts: HashMap::new(),
79        }
80    }
81
82    /// Populate from all `#[chronon::script]` descriptors linked into the binary.
83    ///
84    /// Equivalent to `ChrononBuilder::auto_registry()` wiring. Prefer this on Mode 2 workers.
85    pub fn from_inventory() -> Self {
86        let mut registry = Self::new();
87        registry.register_from_inventory();
88        registry
89    }
90
91    /// Register every script descriptor collected via link-time inventory.
92    pub fn register_from_inventory(&mut self) {
93        for desc in quark::inventory::iter::<ScriptDescriptor> {
94            self.register(ScriptDescriptor::with_signature(
95                desc.name,
96                desc.invoke,
97                desc.signature_json,
98                desc.signature_hash,
99            ));
100        }
101    }
102
103    /// Inserts or replaces a script descriptor keyed by [`ScriptDescriptor::name`].
104    ///
105    /// Duplicate names overwrite the previous handler without error.
106    pub fn register(&mut self, desc: ScriptDescriptor) {
107        self.scripts.insert(
108            desc.name.to_string(),
109            StoredScript {
110                name: desc.name.to_string(),
111                invoke: desc.invoke,
112                signature_json: desc.signature_json.to_string(),
113                signature_hash: desc.signature_hash,
114            },
115        );
116    }
117
118    /// Returns a borrowed view of the script named `name`, if registered.
119    pub fn get(&self, name: &str) -> Option<ScriptDescriptorRef<'_>> {
120        self.scripts.get(name).map(|s| ScriptDescriptorRef {
121            name: s.name.as_str(),
122            invoke: s.invoke,
123            signature_json: s.signature_json.as_str(),
124            signature_hash: s.signature_hash,
125        })
126    }
127
128    /// Like [`Self::get`], but returns [`ChrononError::ScriptNotFound`] when absent.
129    pub fn get_or_err(&self, name: &str) -> Result<ScriptDescriptorRef<'_>> {
130        self.get(name)
131            .ok_or_else(|| ChrononError::ScriptNotFound(name.to_string()))
132    }
133
134    /// Returns the number of registered scripts.
135    pub fn len(&self) -> usize {
136        self.scripts.len()
137    }
138
139    /// Returns `true` when no scripts are registered.
140    pub fn is_empty(&self) -> bool {
141        self.scripts.is_empty()
142    }
143
144    /// Returns `true` when a script with the given name is registered.
145    pub fn contains(&self, name: &str) -> bool {
146        self.scripts.contains_key(name)
147    }
148
149    /// Returns all registered scripts sorted by name.
150    pub fn list(&self) -> Vec<ScriptDescriptorRef<'_>> {
151        let mut v: Vec<_> = self.scripts.values().map(stored_to_ref).collect();
152        v.sort_by_key(|d| d.name);
153        v
154    }
155}
156
157/// Borrowed view of a registered script suitable for invocation.
158#[derive(Clone, Copy)]
159pub struct ScriptDescriptorRef<'a> {
160    /// Registered script name (matches job `script_name`).
161    pub name: &'a str,
162    /// Handler function pointer collected from inventory or manual registration.
163    pub invoke: InvokeFn,
164    /// JSON schema string for handler parameters (from `chronon-macros` when available).
165    pub signature_json: &'a str,
166    /// Compile-time hash of the signature for drift detection.
167    pub signature_hash: u64,
168}
169
170fn stored_to_ref(s: &StoredScript) -> ScriptDescriptorRef<'_> {
171    ScriptDescriptorRef {
172        name: s.name.as_str(),
173        invoke: s.invoke,
174        signature_json: s.signature_json.as_str(),
175        signature_hash: s.signature_hash,
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::descriptor::ScriptDescriptor;
183    use chronon_core::{Result, ScriptContext};
184    use serde_json::Value;
185    use std::future::Future;
186    use std::pin::Pin;
187
188    fn stub_invoke(
189        _ctx: Box<dyn ScriptContext>,
190        _params: Value,
191    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
192        Box::pin(async { Ok(()) })
193    }
194
195    #[test]
196    fn register_and_lookup() {
197        let mut reg = ScriptRegistry::new();
198        reg.register(ScriptDescriptor::new("hello", stub_invoke));
199        assert!(reg.contains("hello"));
200        assert_eq!(reg.len(), 1);
201    }
202}