1use std::sync::Arc;
2
3use tokio::task::JoinHandle;
4use tracing::info;
5
6use crate::auth_mode::AuthMode;
7use crate::dispatch_recovery;
8use crate::health;
9use crate::scheduler;
10use crate::store::WorkflowStore;
11use crate::timers;
12
13#[derive(Clone, Debug)]
18pub struct EngineEvent {
19 pub event_type: String,
20 pub workflow_id: String,
21 pub namespace: String,
22}
23
24#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
26pub struct BroadcastEvent {
27 pub event_type: String,
28 pub workflow_id: String,
29 pub payload: Option<String>,
30}
31
32pub struct BackgroundTasks {
35 _scheduler: JoinHandle<()>,
36 _timer_poller: JoinHandle<()>,
37 _health_monitor: JoinHandle<()>,
38 _dispatch_recovery: JoinHandle<()>,
39 #[cfg(feature = "s3-archival")]
40 _archival: Option<JoinHandle<()>>,
41}
42
43pub struct WorkflowCtx<S: WorkflowStore> {
51 pub(crate) store: Arc<S>,
52 pub(crate) event_tx: Option<tokio::sync::broadcast::Sender<EngineEvent>>,
55 pub sse_tx: Option<tokio::sync::broadcast::Sender<BroadcastEvent>>,
57 pub(crate) _bg: Arc<BackgroundTasks>,
58 pub auth_mode: AuthMode,
59 pub binary_version: Option<&'static str>,
63}
64
65impl<S: WorkflowStore> WorkflowCtx<S> {
66 pub fn start(store: Arc<S>) -> Self {
68 let _scheduler = tokio::spawn(scheduler::run_scheduler(Arc::clone(&store)));
69 let _timer_poller = tokio::spawn(timers::run_timer_poller(Arc::clone(&store)));
70 let _health_monitor = tokio::spawn(health::run_health_monitor(Arc::clone(&store)));
71 let _dispatch_recovery = tokio::spawn(dispatch_recovery::run_dispatch_recovery(
72 Arc::clone(&store),
73 ));
74
75 #[cfg(feature = "s3-archival")]
76 let _archival = crate::archival::ArchivalConfig::from_env().map(|cfg| {
77 tokio::spawn(crate::archival::run_archival(Arc::clone(&store), cfg))
78 });
79
80 info!("Workflow engine started");
81
82 Self {
83 store,
84 event_tx: None,
85 sse_tx: None,
86 _bg: Arc::new(BackgroundTasks {
87 _scheduler,
88 _timer_poller,
89 _health_monitor,
90 _dispatch_recovery,
91 #[cfg(feature = "s3-archival")]
92 _archival,
93 }),
94 auth_mode: AuthMode::default(),
95 binary_version: None,
96 }
97 }
98
99 pub fn with_event_broadcaster(
108 mut self,
109 tx: tokio::sync::broadcast::Sender<EngineEvent>,
110 ) -> Self {
111 self.event_tx = Some(tx);
112 self
113 }
114
115 pub fn with_sse_tx(
117 mut self,
118 tx: tokio::sync::broadcast::Sender<BroadcastEvent>,
119 ) -> Self {
120 self.sse_tx = Some(tx);
121 self
122 }
123
124 pub fn with_auth_mode(mut self, auth_mode: AuthMode) -> Self {
126 self.auth_mode = auth_mode;
127 self
128 }
129
130 pub fn with_binary_version(mut self, version: &'static str) -> Self {
132 self.binary_version = Some(version);
133 self
134 }
135
136 pub fn store(&self) -> &S {
138 &self.store
139 }
140
141 pub(crate) fn broadcast(&self, event_type: &str, workflow_id: &str, namespace: &str) {
146 if let Some(tx) = &self.event_tx {
147 let _ = tx.send(EngineEvent {
148 event_type: event_type.to_string(),
149 workflow_id: workflow_id.to_string(),
150 namespace: namespace.to_string(),
151 });
152 }
153 }
154}
155
156pub(crate) fn strip_continued_suffix(id: &str) -> &str {
161 if let Some(idx) = id.rfind("-continued-") {
162 let (head, tail) = id.split_at(idx);
163 let rest = &tail["-continued-".len()..];
165 if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) {
166 return head;
167 }
168 }
169 id
170}
171
172pub(crate) fn timestamp_now() -> f64 {
173 std::time::SystemTime::now()
174 .duration_since(std::time::UNIX_EPOCH)
175 .unwrap()
176 .as_secs_f64()
177}
178
179pub(crate) const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
183
184pub(crate) fn inject_engine_version(caller_attrs: Option<&str>) -> Option<String> {
192 let mut obj: serde_json::Map<String, serde_json::Value> = match caller_attrs {
193 Some(raw) => match serde_json::from_str::<serde_json::Value>(raw) {
194 Ok(serde_json::Value::Object(m)) => m,
195 Ok(other) => return Some(other.to_string()),
198 Err(_) => return Some(raw.to_string()),
199 },
200 None => serde_json::Map::new(),
201 };
202 obj.entry("assay_engine_version".to_string())
203 .or_insert_with(|| serde_json::Value::String(ENGINE_VERSION.to_string()));
204 Some(serde_json::Value::Object(obj).to_string())
205}
206
207#[cfg(test)]
208mod engine_version_stamp_tests {
209 use super::*;
210
211 #[test]
212 fn no_attrs_produces_single_key_object() {
213 let out = inject_engine_version(None).unwrap();
214 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
215 assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
216 assert_eq!(v.as_object().unwrap().len(), 1);
217 }
218
219 #[test]
220 fn existing_attrs_gain_the_version_field() {
221 let out = inject_engine_version(Some(r#"{"env":"prod","tenant":"acme"}"#)).unwrap();
222 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
223 assert_eq!(v["env"], "prod");
224 assert_eq!(v["tenant"], "acme");
225 assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
226 }
227
228 #[test]
229 fn caller_supplied_version_wins_on_conflict() {
230 let out = inject_engine_version(Some(r#"{"assay_engine_version":"0.0.1-test"}"#)).unwrap();
231 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
232 assert_eq!(v["assay_engine_version"], "0.0.1-test");
233 }
234
235 #[test]
236 fn non_object_json_is_preserved_unchanged() {
237 let out = inject_engine_version(Some("[1, 2, 3]")).unwrap();
238 assert_eq!(out, "[1,2,3]");
239 }
240
241 #[test]
242 fn unparsable_json_is_preserved_unchanged() {
243 let out = inject_engine_version(Some("not json")).unwrap();
244 assert_eq!(out, "not json");
245 }
246}