1use std::sync::Arc;
2
3use tokio::task::JoinHandle;
4use tracing::info;
5
6use crate::auth_mode::AuthMode;
7use crate::dispatch_recovery;
8use crate::events::{WorkflowBusEvent, WorkflowEventBus};
9use crate::health;
10use crate::scheduler;
11use crate::store::WorkflowStore;
12use crate::timers;
13
14pub struct BackgroundTasks {
17 _scheduler: JoinHandle<()>,
18 _timer_poller: JoinHandle<()>,
19 _health_monitor: JoinHandle<()>,
20 _dispatch_recovery: JoinHandle<()>,
21 #[cfg(feature = "s3-archival")]
22 _archival: Option<JoinHandle<()>>,
23}
24
25pub struct WorkflowCtx<S: WorkflowStore> {
33 pub(crate) store: Arc<S>,
34 pub(crate) bus: Option<WorkflowEventBus>,
39 pub(crate) _bg: Arc<BackgroundTasks>,
40 pub auth_mode: AuthMode,
41 pub binary_version: Option<&'static str>,
45}
46
47impl<S: WorkflowStore> WorkflowCtx<S> {
48 pub fn start(store: Arc<S>) -> Self {
50 let _scheduler = tokio::spawn(scheduler::run_scheduler(Arc::clone(&store)));
51 let _timer_poller = tokio::spawn(timers::run_timer_poller(Arc::clone(&store)));
52 let _health_monitor = tokio::spawn(health::run_health_monitor(Arc::clone(&store)));
53 let _dispatch_recovery = tokio::spawn(dispatch_recovery::run_dispatch_recovery(
54 Arc::clone(&store),
55 ));
56
57 #[cfg(feature = "s3-archival")]
58 let _archival = crate::archival::ArchivalConfig::from_env()
59 .map(|cfg| tokio::spawn(crate::archival::run_archival(Arc::clone(&store), cfg)));
60
61 info!("Workflow engine started");
62
63 Self {
64 store,
65 bus: None,
66 _bg: Arc::new(BackgroundTasks {
67 _scheduler,
68 _timer_poller,
69 _health_monitor,
70 _dispatch_recovery,
71 #[cfg(feature = "s3-archival")]
72 _archival,
73 }),
74 auth_mode: AuthMode::default(),
75 binary_version: None,
76 }
77 }
78
79 pub fn with_event_bus(mut self, bus: WorkflowEventBus) -> Self {
84 self.bus = Some(bus);
85 self
86 }
87
88 pub fn with_auth_mode(mut self, auth_mode: AuthMode) -> Self {
90 self.auth_mode = auth_mode;
91 self
92 }
93
94 pub fn with_binary_version(mut self, version: &'static str) -> Self {
96 self.binary_version = Some(version);
97 self
98 }
99
100 pub fn store(&self) -> &S {
102 &self.store
103 }
104
105 pub fn bus(&self) -> Option<&WorkflowEventBus> {
107 self.bus.as_ref()
108 }
109
110 pub(crate) async fn emit(&self, namespace: &str, ev: WorkflowBusEvent) {
116 if let Some(bus) = &self.bus
117 && let Err(e) = bus.publish(namespace, ev).await
118 {
119 tracing::warn!(?e, "engine event emit failed");
120 }
121 }
122
123 pub(crate) async fn mark_and_emit_needs_dispatch(
128 &self,
129 workflow_id: &str,
130 ) -> anyhow::Result<()> {
131 self.store.mark_workflow_dispatchable(workflow_id).await?;
132 if self.bus.is_some()
133 && let Some(wf) = self.store.get_workflow(workflow_id).await?
134 {
135 self.emit(
136 &wf.namespace,
137 WorkflowBusEvent::WorkflowNeedsDispatch {
138 workflow_id: workflow_id.to_string(),
139 task_queue: wf.task_queue,
140 },
141 )
142 .await;
143 }
144 Ok(())
145 }
146}
147
148pub(crate) fn strip_continued_suffix(id: &str) -> &str {
153 if let Some(idx) = id.rfind("-continued-") {
154 let (head, tail) = id.split_at(idx);
155 let rest = &tail["-continued-".len()..];
156 if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) {
157 return head;
158 }
159 }
160 id
161}
162
163pub(crate) fn timestamp_now() -> f64 {
164 std::time::SystemTime::now()
165 .duration_since(std::time::UNIX_EPOCH)
166 .unwrap()
167 .as_secs_f64()
168}
169
170pub(crate) const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
174
175pub(crate) fn inject_engine_version(caller_attrs: Option<&str>) -> Option<String> {
183 let mut obj: serde_json::Map<String, serde_json::Value> = match caller_attrs {
184 Some(raw) => match serde_json::from_str::<serde_json::Value>(raw) {
185 Ok(serde_json::Value::Object(m)) => m,
186 Ok(other) => return Some(other.to_string()),
187 Err(_) => return Some(raw.to_string()),
188 },
189 None => serde_json::Map::new(),
190 };
191 obj.entry("assay_engine_version".to_string())
192 .or_insert_with(|| serde_json::Value::String(ENGINE_VERSION.to_string()));
193 Some(serde_json::Value::Object(obj).to_string())
194}
195
196#[cfg(test)]
197mod engine_version_stamp_tests {
198 use super::*;
199
200 #[test]
201 fn no_attrs_produces_single_key_object() {
202 let out = inject_engine_version(None).unwrap();
203 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
204 assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
205 assert_eq!(v.as_object().unwrap().len(), 1);
206 }
207
208 #[test]
209 fn existing_attrs_gain_the_version_field() {
210 let out = inject_engine_version(Some(r#"{"env":"prod","tenant":"acme"}"#)).unwrap();
211 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
212 assert_eq!(v["env"], "prod");
213 assert_eq!(v["tenant"], "acme");
214 assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
215 }
216
217 #[test]
218 fn caller_supplied_version_wins_on_conflict() {
219 let out =
220 inject_engine_version(Some(r#"{"assay_engine_version":"0.0.1-test"}"#)).unwrap();
221 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
222 assert_eq!(v["assay_engine_version"], "0.0.1-test");
223 }
224
225 #[test]
226 fn non_object_json_is_preserved_unchanged() {
227 let out = inject_engine_version(Some("[1, 2, 3]")).unwrap();
228 assert_eq!(out, "[1,2,3]");
229 }
230
231 #[test]
232 fn unparsable_json_is_preserved_unchanged() {
233 let out = inject_engine_version(Some("not json")).unwrap();
234 assert_eq!(out, "not json");
235 }
236}