1mod descriptor;
18mod invoke;
19mod registry;
20
21pub use descriptor::{InvokeFn, ScriptDescriptor};
22pub use invoke::{execute_script, ExecuteScriptRequest};
23pub use registry::{ScriptDescriptorRef, ScriptRegistry};
24
25use std::sync::Arc;
26
27use chrono::Utc;
28use chronon_core::{ContextFactory, Job, Run};
29use chronon_telemetry::TelemetrySink;
30use tokio::sync::mpsc;
31
32#[derive(Debug, Clone)]
36pub enum ExecutorEvent {
37 RunStarted {
39 run_id: String,
41 },
42 RunCompleted {
44 run_id: String,
46 duration_ms: i64,
48 },
49 RunFailed {
51 run_id: String,
53 error: String,
55 },
56}
57
58pub struct Executor {
62 pub registry: Arc<ScriptRegistry>,
64 pub context_factory: Arc<dyn ContextFactory>,
66 pub telemetry: Arc<dyn TelemetrySink>,
68 event_tx: mpsc::UnboundedSender<ExecutorEvent>,
69}
70
71impl Executor {
72 pub fn new(
77 registry: Arc<ScriptRegistry>,
78 context_factory: Arc<dyn ContextFactory>,
79 telemetry: Arc<dyn TelemetrySink>,
80 event_tx: mpsc::UnboundedSender<ExecutorEvent>,
81 ) -> Self {
82 Self {
83 registry,
84 context_factory,
85 telemetry,
86 event_tx,
87 }
88 }
89
90 pub fn event_sender(&self) -> mpsc::UnboundedSender<ExecutorEvent> {
94 self.event_tx.clone()
95 }
96
97 pub fn script_count(&self) -> usize {
99 self.registry.len()
100 }
101
102 pub fn spawn_run(&self, job: &Job, run: Run) {
107 let registry = Arc::clone(&self.registry);
108 let context_factory = Arc::clone(&self.context_factory);
109 let telemetry = Arc::clone(&self.telemetry);
110 let event_tx = self.event_tx.clone();
111
112 let script_name = job.script_name.clone();
113 let job_name = job.job_name.clone();
114 let params_json = run.params_json.clone();
115 let actor_json = job.actor_json.clone();
116 let run_id = run.run_id;
117
118 tokio::spawn(async move {
119 let _ = event_tx.send(ExecutorEvent::RunStarted {
120 run_id: run_id.clone(),
121 });
122 telemetry.record_counter(
123 "chronon_runs_started",
124 &[("script", script_name.as_str()), ("job", job_name.as_str())],
125 1,
126 );
127
128 let started = Utc::now();
129 let result = invoke::execute_script(invoke::ExecuteScriptRequest {
130 registry: ®istry,
131 context_factory: &context_factory,
132 telemetry: &telemetry,
133 script_name: &script_name,
134 actor_json: &actor_json,
135 params_json,
136 job_name: &job_name,
137 run_id: &run_id,
138 })
139 .await;
140
141 let duration_ms = (Utc::now() - started).num_milliseconds();
142 match result {
143 Ok(()) => {
144 let _ = event_tx.send(ExecutorEvent::RunCompleted {
145 run_id: run_id.clone(),
146 duration_ms,
147 });
148 telemetry.record_counter(
149 "chronon_runs_completed",
150 &[("script", script_name.as_str()), ("job", job_name.as_str())],
151 1,
152 );
153 }
154 Err(e) => {
155 let error_msg = e.to_string();
156 let _ = event_tx.send(ExecutorEvent::RunFailed {
157 run_id: run_id.clone(),
158 error: error_msg.clone(),
159 });
160 telemetry.record_counter(
161 "chronon_runs_failed",
162 &[("script", script_name.as_str()), ("job", job_name.as_str())],
163 1,
164 );
165 telemetry.log_event(
166 "chronon_run_failed",
167 &[
168 ("run_id", run_id.as_str()),
169 ("job", job_name.as_str()),
170 ("error", error_msg.as_str()),
171 ],
172 );
173 }
174 }
175 });
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use chronon_core::{NoOpContextFactory, Result, ScriptContext};
183 use serde_json::{json, Value};
184 use std::future::Future;
185 use std::pin::Pin;
186 use std::sync::Mutex;
187
188 static LAST_PARAMS: Mutex<Option<Value>> = Mutex::new(None);
189
190 fn param_probe(
191 _ctx: Box<dyn ScriptContext>,
192 params: Value,
193 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
194 Box::pin(async move {
195 *LAST_PARAMS.lock().unwrap() = Some(params);
196 Ok(())
197 })
198 }
199
200 #[tokio::test]
201 async fn spawn_run_uses_run_params() {
202 *LAST_PARAMS.lock().unwrap() = None;
203 let registry = Arc::new({
204 let mut r = ScriptRegistry::new();
205 r.register(ScriptDescriptor::new("probe", param_probe));
206 r
207 });
208 let (tx, mut rx) = mpsc::unbounded_channel();
209 let executor = Executor::new(
210 registry,
211 Arc::new(NoOpContextFactory),
212 Arc::new(chronon_telemetry::NoOpSink),
213 tx,
214 );
215
216 let mut job = Job::new("job", "probe");
217 let mut run = chronon_core::Run::for_job(&job.job_id, "probe", Utc::now());
218 run.params_json = json!({ "source": "run" });
219 job.params_json = json!({ "source": "job" });
220
221 executor.spawn_run(&job, run);
222
223 for _ in 0..20 {
224 if let Some(ExecutorEvent::RunCompleted { .. }) = rx.recv().await {
225 break;
226 }
227 }
228 assert_eq!(
229 *LAST_PARAMS.lock().unwrap(),
230 Some(json!({ "source": "run" }))
231 );
232 }
233}