mod descriptor;
mod invoke;
mod registry;
pub use descriptor::{InvokeFn, ScriptDescriptor};
pub use invoke::{execute_script, ExecuteScriptRequest};
pub use registry::{ScriptDescriptorRef, ScriptRegistry};
use std::sync::Arc;
use chrono::Utc;
use chronon_core::{ContextFactory, Job, Run};
use chronon_telemetry::TelemetrySink;
use tokio::sync::mpsc;
#[derive(Debug, Clone)]
pub enum ExecutorEvent {
RunStarted {
run_id: String,
},
RunCompleted {
run_id: String,
duration_ms: i64,
},
RunFailed {
run_id: String,
error: String,
},
}
pub struct Executor {
pub registry: Arc<ScriptRegistry>,
pub context_factory: Arc<dyn ContextFactory>,
pub telemetry: Arc<dyn TelemetrySink>,
event_tx: mpsc::UnboundedSender<ExecutorEvent>,
}
impl Executor {
pub fn new(
registry: Arc<ScriptRegistry>,
context_factory: Arc<dyn ContextFactory>,
telemetry: Arc<dyn TelemetrySink>,
event_tx: mpsc::UnboundedSender<ExecutorEvent>,
) -> Self {
Self {
registry,
context_factory,
telemetry,
event_tx,
}
}
pub fn event_sender(&self) -> mpsc::UnboundedSender<ExecutorEvent> {
self.event_tx.clone()
}
pub fn script_count(&self) -> usize {
self.registry.len()
}
pub fn spawn_run(&self, job: &Job, run: Run) {
let registry = Arc::clone(&self.registry);
let context_factory = Arc::clone(&self.context_factory);
let telemetry = Arc::clone(&self.telemetry);
let event_tx = self.event_tx.clone();
let script_name = job.script_name.clone();
let job_name = job.job_name.clone();
let params_json = run.params_json.clone();
let actor_json = job.actor_json.clone();
let run_id = run.run_id;
tokio::spawn(async move {
let _ = event_tx.send(ExecutorEvent::RunStarted {
run_id: run_id.clone(),
});
telemetry.record_counter(
"chronon_runs_started",
&[("script", script_name.as_str()), ("job", job_name.as_str())],
1,
);
let started = Utc::now();
let result = invoke::execute_script(invoke::ExecuteScriptRequest {
registry: ®istry,
context_factory: &context_factory,
telemetry: &telemetry,
script_name: &script_name,
actor_json: &actor_json,
params_json,
job_name: &job_name,
run_id: &run_id,
})
.await;
let duration_ms = (Utc::now() - started).num_milliseconds();
match result {
Ok(()) => {
let _ = event_tx.send(ExecutorEvent::RunCompleted {
run_id: run_id.clone(),
duration_ms,
});
telemetry.record_counter(
"chronon_runs_completed",
&[("script", script_name.as_str()), ("job", job_name.as_str())],
1,
);
}
Err(e) => {
let error_msg = e.to_string();
let _ = event_tx.send(ExecutorEvent::RunFailed {
run_id: run_id.clone(),
error: error_msg.clone(),
});
telemetry.record_counter(
"chronon_runs_failed",
&[("script", script_name.as_str()), ("job", job_name.as_str())],
1,
);
telemetry.log_event(
"chronon_run_failed",
&[
("run_id", run_id.as_str()),
("job", job_name.as_str()),
("error", error_msg.as_str()),
],
);
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use chronon_core::{NoOpContextFactory, Result, ScriptContext};
use serde_json::{json, Value};
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex;
static LAST_PARAMS: Mutex<Option<Value>> = Mutex::new(None);
fn param_probe(
_ctx: Box<dyn ScriptContext>,
params: Value,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
Box::pin(async move {
*LAST_PARAMS.lock().unwrap() = Some(params);
Ok(())
})
}
#[tokio::test]
async fn spawn_run_uses_run_params() {
*LAST_PARAMS.lock().unwrap() = None;
let registry = Arc::new({
let mut r = ScriptRegistry::new();
r.register(ScriptDescriptor::new("probe", param_probe));
r
});
let (tx, mut rx) = mpsc::unbounded_channel();
let executor = Executor::new(
registry,
Arc::new(NoOpContextFactory),
Arc::new(chronon_telemetry::NoOpSink),
tx,
);
let mut job = Job::new("job", "probe");
let mut run = chronon_core::Run::for_job(&job.job_id, "probe", Utc::now());
run.params_json = json!({ "source": "run" });
job.params_json = json!({ "source": "job" });
executor.spawn_run(&job, run);
for _ in 0..20 {
if let Some(ExecutorEvent::RunCompleted { .. }) = rx.recv().await {
break;
}
}
assert_eq!(
*LAST_PARAMS.lock().unwrap(),
Some(json!({ "source": "run" }))
);
}
}