use std::sync::Arc;
use tokio::task::JoinHandle;
use tracing::info;
use crate::dispatch_recovery;
use crate::events::{WorkflowBusEvent, WorkflowEventBus};
use crate::health;
use crate::scheduler;
use crate::store::WorkflowStore;
use crate::timers;
pub struct BackgroundTasks {
_scheduler: JoinHandle<()>,
_timer_poller: JoinHandle<()>,
_health_monitor: JoinHandle<()>,
_dispatch_recovery: JoinHandle<()>,
#[cfg(feature = "s3-archival")]
_archival: Option<JoinHandle<()>>,
}
pub struct WorkflowCtx<S: WorkflowStore> {
pub(crate) store: Arc<S>,
pub(crate) bus: Option<WorkflowEventBus>,
pub(crate) _bg: Arc<BackgroundTasks>,
pub binary_version: Option<&'static str>,
}
impl<S: WorkflowStore> WorkflowCtx<S> {
pub fn start(store: Arc<S>) -> Self {
let _scheduler = tokio::spawn(scheduler::run_scheduler(Arc::clone(&store)));
let _timer_poller = tokio::spawn(timers::run_timer_poller(Arc::clone(&store)));
let _health_monitor = tokio::spawn(health::run_health_monitor(Arc::clone(&store)));
let _dispatch_recovery =
tokio::spawn(dispatch_recovery::run_dispatch_recovery(Arc::clone(&store)));
#[cfg(feature = "s3-archival")]
let _archival = crate::archival::ArchivalConfig::from_env()
.map(|cfg| tokio::spawn(crate::archival::run_archival(Arc::clone(&store), cfg)));
info!("Workflow engine started");
Self {
store,
bus: None,
_bg: Arc::new(BackgroundTasks {
_scheduler,
_timer_poller,
_health_monitor,
_dispatch_recovery,
#[cfg(feature = "s3-archival")]
_archival,
}),
binary_version: None,
}
}
pub fn with_event_bus(mut self, bus: WorkflowEventBus) -> Self {
self.bus = Some(bus);
self
}
pub fn with_binary_version(mut self, version: &'static str) -> Self {
self.binary_version = Some(version);
self
}
pub fn store(&self) -> &S {
&self.store
}
pub fn bus(&self) -> Option<&WorkflowEventBus> {
self.bus.as_ref()
}
pub(crate) async fn emit(&self, namespace: &str, ev: WorkflowBusEvent) {
if let Some(bus) = &self.bus
&& let Err(e) = bus.publish(namespace, ev).await
{
tracing::warn!(?e, "engine event emit failed");
}
}
pub(crate) async fn mark_and_emit_needs_dispatch(
&self,
workflow_id: &str,
) -> anyhow::Result<()> {
self.store.mark_workflow_dispatchable(workflow_id).await?;
if self.bus.is_some()
&& let Some(wf) = self.store.get_workflow(workflow_id).await?
{
self.emit(
&wf.namespace,
WorkflowBusEvent::WorkflowNeedsDispatch {
workflow_id: workflow_id.to_string(),
task_queue: wf.task_queue,
},
)
.await;
}
Ok(())
}
}
pub(crate) fn strip_continued_suffix(id: &str) -> &str {
if let Some(idx) = id.rfind("-continued-") {
let (head, tail) = id.split_at(idx);
let rest = &tail["-continued-".len()..];
if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) {
return head;
}
}
id
}
pub(crate) fn timestamp_now() -> f64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64()
}
pub(crate) const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) fn inject_engine_version(caller_attrs: Option<&str>) -> Option<String> {
let mut obj: serde_json::Map<String, serde_json::Value> = match caller_attrs {
Some(raw) => match serde_json::from_str::<serde_json::Value>(raw) {
Ok(serde_json::Value::Object(m)) => m,
Ok(other) => return Some(other.to_string()),
Err(_) => return Some(raw.to_string()),
},
None => serde_json::Map::new(),
};
obj.entry("assay_engine_version".to_string())
.or_insert_with(|| serde_json::Value::String(ENGINE_VERSION.to_string()));
Some(serde_json::Value::Object(obj).to_string())
}
#[cfg(test)]
mod engine_version_stamp_tests {
use super::*;
#[test]
fn no_attrs_produces_single_key_object() {
let out = inject_engine_version(None).unwrap();
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
assert_eq!(v.as_object().unwrap().len(), 1);
}
#[test]
fn existing_attrs_gain_the_version_field() {
let out = inject_engine_version(Some(r#"{"env":"prod","tenant":"acme"}"#)).unwrap();
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["env"], "prod");
assert_eq!(v["tenant"], "acme");
assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
}
#[test]
fn caller_supplied_version_wins_on_conflict() {
let out = inject_engine_version(Some(r#"{"assay_engine_version":"0.0.1-test"}"#)).unwrap();
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["assay_engine_version"], "0.0.1-test");
}
#[test]
fn non_object_json_is_preserved_unchanged() {
let out = inject_engine_version(Some("[1, 2, 3]")).unwrap();
assert_eq!(out, "[1,2,3]");
}
#[test]
fn unparsable_json_is_preserved_unchanged() {
let out = inject_engine_version(Some("not json")).unwrap();
assert_eq!(out, "not json");
}
}