1use std::sync::Arc;
2
3use tokio::task::JoinHandle;
4use tracing::info;
5
6use crate::dispatch_recovery;
7use crate::events::{WorkflowBusEvent, WorkflowEventBus};
8use crate::health;
9use crate::scheduler;
10use crate::store::WorkflowStore;
11use crate::timers;
12
13pub struct BackgroundTasks {
16 _scheduler: JoinHandle<()>,
17 _timer_poller: JoinHandle<()>,
18 _health_monitor: JoinHandle<()>,
19 _dispatch_recovery: JoinHandle<()>,
20 #[cfg(feature = "s3-archival")]
21 _archival: Option<JoinHandle<()>>,
22}
23
24pub struct WorkflowCtx<S: WorkflowStore> {
32 pub(crate) store: Arc<S>,
33 pub(crate) bus: Option<WorkflowEventBus>,
38 pub(crate) _bg: Arc<BackgroundTasks>,
39 pub binary_version: Option<&'static str>,
43}
44
45impl<S: WorkflowStore> WorkflowCtx<S> {
46 pub fn start(store: Arc<S>) -> Self {
48 let _scheduler = tokio::spawn(scheduler::run_scheduler(Arc::clone(&store)));
49 let _timer_poller = tokio::spawn(timers::run_timer_poller(Arc::clone(&store)));
50 let _health_monitor = tokio::spawn(health::run_health_monitor(Arc::clone(&store)));
51 let _dispatch_recovery =
52 tokio::spawn(dispatch_recovery::run_dispatch_recovery(Arc::clone(&store)));
53
54 #[cfg(feature = "s3-archival")]
55 let _archival = crate::archival::ArchivalConfig::from_env()
56 .map(|cfg| tokio::spawn(crate::archival::run_archival(Arc::clone(&store), cfg)));
57
58 info!("Workflow engine started");
59
60 Self {
61 store,
62 bus: None,
63 _bg: Arc::new(BackgroundTasks {
64 _scheduler,
65 _timer_poller,
66 _health_monitor,
67 _dispatch_recovery,
68 #[cfg(feature = "s3-archival")]
69 _archival,
70 }),
71 binary_version: None,
72 }
73 }
74
75 pub fn with_event_bus(mut self, bus: WorkflowEventBus) -> Self {
80 self.bus = Some(bus);
81 self
82 }
83
84 pub fn with_binary_version(mut self, version: &'static str) -> Self {
86 self.binary_version = Some(version);
87 self
88 }
89
90 pub fn store(&self) -> &S {
92 &self.store
93 }
94
95 pub fn bus(&self) -> Option<&WorkflowEventBus> {
97 self.bus.as_ref()
98 }
99
100 pub(crate) async fn emit(&self, namespace: &str, ev: WorkflowBusEvent) {
106 if let Some(bus) = &self.bus
107 && let Err(e) = bus.publish(namespace, ev).await
108 {
109 tracing::warn!(?e, "engine event emit failed");
110 }
111 }
112
113 pub(crate) async fn emit_retry_requested(
114 &self,
115 namespace: &str,
116 workflow_id: &str,
117 activity_id: i64,
118 activity_seq: i32,
119 ) {
120 if let Some(bus) = &self.bus
121 && let Err(e) = bus
122 .publish_retry_requested(namespace, workflow_id, activity_id, activity_seq)
123 .await
124 {
125 tracing::warn!(?e, "engine retry event emit failed");
126 }
127 }
128
129 pub(crate) async fn mark_and_emit_needs_dispatch(
134 &self,
135 workflow_id: &str,
136 ) -> anyhow::Result<()> {
137 self.store.mark_workflow_dispatchable(workflow_id).await?;
138 self.emit_needs_dispatch(workflow_id).await;
139 Ok(())
140 }
141
142 pub(crate) async fn emit_needs_dispatch(&self, workflow_id: &str) {
148 if self.bus.is_none() {
149 return;
150 }
151 match self.store.get_workflow(workflow_id).await {
152 Ok(Some(wf)) => {
153 self.emit(
154 &wf.namespace,
155 WorkflowBusEvent::WorkflowNeedsDispatch {
156 workflow_id: workflow_id.to_string(),
157 task_queue: wf.task_queue,
158 },
159 )
160 .await;
161 }
162 Ok(None) => {}
163 Err(e) => tracing::warn!(?e, "needs-dispatch emit lookup failed"),
164 }
165 }
166}
167
168pub(crate) fn strip_continued_suffix(id: &str) -> &str {
173 if let Some(idx) = id.rfind("-continued-") {
174 let (head, tail) = id.split_at(idx);
175 let rest = &tail["-continued-".len()..];
176 if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) {
177 return head;
178 }
179 }
180 id
181}
182
183pub(crate) fn timestamp_now() -> f64 {
184 std::time::SystemTime::now()
185 .duration_since(std::time::UNIX_EPOCH)
186 .unwrap()
187 .as_secs_f64()
188}
189
190pub(crate) const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
194
195pub(crate) fn inject_engine_version(caller_attrs: Option<&str>) -> Option<String> {
203 let mut obj: serde_json::Map<String, serde_json::Value> = match caller_attrs {
204 Some(raw) => match serde_json::from_str::<serde_json::Value>(raw) {
205 Ok(serde_json::Value::Object(m)) => m,
206 Ok(other) => return Some(other.to_string()),
207 Err(_) => return Some(raw.to_string()),
208 },
209 None => serde_json::Map::new(),
210 };
211 obj.entry("assay_engine_version".to_string())
212 .or_insert_with(|| serde_json::Value::String(ENGINE_VERSION.to_string()));
213 Some(serde_json::Value::Object(obj).to_string())
214}
215
216#[cfg(test)]
217mod engine_version_stamp_tests {
218 use super::*;
219
220 #[test]
221 fn no_attrs_produces_single_key_object() {
222 let out = inject_engine_version(None).unwrap();
223 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
224 assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
225 assert_eq!(v.as_object().unwrap().len(), 1);
226 }
227
228 #[test]
229 fn existing_attrs_gain_the_version_field() {
230 let out = inject_engine_version(Some(r#"{"env":"prod","tenant":"acme"}"#)).unwrap();
231 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
232 assert_eq!(v["env"], "prod");
233 assert_eq!(v["tenant"], "acme");
234 assert_eq!(v["assay_engine_version"], ENGINE_VERSION);
235 }
236
237 #[test]
238 fn caller_supplied_version_wins_on_conflict() {
239 let out = inject_engine_version(Some(r#"{"assay_engine_version":"0.0.1-test"}"#)).unwrap();
240 let v: serde_json::Value = serde_json::from_str(&out).unwrap();
241 assert_eq!(v["assay_engine_version"], "0.0.1-test");
242 }
243
244 #[test]
245 fn non_object_json_is_preserved_unchanged() {
246 let out = inject_engine_version(Some("[1, 2, 3]")).unwrap();
247 assert_eq!(out, "[1,2,3]");
248 }
249
250 #[test]
251 fn unparsable_json_is_preserved_unchanged() {
252 let out = inject_engine_version(Some("not json")).unwrap();
253 assert_eq!(out, "not json");
254 }
255}