1use std::collections::HashMap;
6use std::sync::Arc;
7
8use aion_core::{
9 Event, EventEnvelope, Payload, RunId, ScheduleConfig, ScheduleId, SearchAttributeSchema,
10 SearchAttributeValue, WorkflowId,
11};
12use async_trait::async_trait;
13use chrono::{DateTime, Utc};
14use tokio::sync::Mutex as AsyncMutex;
15
16use aion_store::EventStore;
17use aion_store::visibility::VisibilityStore;
18
19use crate::durability::Recorder;
20use crate::lifecycle::start::{self, StartWorkflowContext};
21use crate::schedule::{
22 NoopScheduleCanceller, ScheduleEvaluator, ScheduleEvaluatorError, ScheduleEventSink,
23 ScheduleEventSource, ScheduleExecution, ScheduleState, ScheduleTimer, ScheduleWorkflowStarter,
24 StoreScheduleTimer, TimerEvaluationOutcome,
25};
26use crate::{EngineError, Registry, RuntimeHandle, SupervisionTree, WorkflowCatalog};
27
28use super::api::Engine;
29
30impl Engine {
31 #[must_use]
33 pub const fn schedule_coordinator_workflow_id(&self) -> &WorkflowId {
34 &self.schedule_coordinator_workflow_id
35 }
36
37 pub async fn create_schedule(&self, config: ScheduleConfig) -> Result<ScheduleId, EngineError> {
43 let operation = self.shutdown_gate.begin_start()?;
44 let recorded_at = Utc::now();
45 let schedule_id = ScheduleId::new_v4();
46 let result = self
47 .create_schedule_inner(schedule_id, config, recorded_at)
48 .await;
49 drop(operation);
50 result
51 }
52
53 pub async fn update_schedule(
61 &self,
62 schedule_id: &ScheduleId,
63 config: ScheduleConfig,
64 ) -> Result<(), EngineError> {
65 let operation = self.shutdown_gate.begin_operation()?;
66 let result = self.update_schedule_inner(schedule_id, config).await;
67 drop(operation);
68 result
69 }
70
71 pub async fn pause_schedule(&self, schedule_id: &ScheduleId) -> Result<(), EngineError> {
79 let operation = self.shutdown_gate.begin_operation()?;
80 let result = self.pause_schedule_inner(schedule_id).await;
81 drop(operation);
82 result
83 }
84
85 pub async fn resume_schedule(&self, schedule_id: &ScheduleId) -> Result<(), EngineError> {
93 let operation = self.shutdown_gate.begin_operation()?;
94 let result = self.resume_schedule_inner(schedule_id).await;
95 drop(operation);
96 result
97 }
98
99 pub async fn delete_schedule(&self, schedule_id: &ScheduleId) -> Result<(), EngineError> {
107 let operation = self.shutdown_gate.begin_operation()?;
108 let result = self.delete_schedule_inner(schedule_id).await;
109 drop(operation);
110 result
111 }
112
113 pub async fn list_schedules(&self) -> Result<Vec<ScheduleState>, EngineError> {
119 let evaluator = self.schedule_evaluator.lock().await;
120 let mut schedules = evaluator
121 .states()
122 .filter(|state| !state.is_deleted)
123 .cloned()
124 .collect::<Vec<_>>();
125 schedules.sort_by(|left, right| {
126 left.next_trigger_at
127 .cmp(&right.next_trigger_at)
128 .then_with(|| {
129 left.schedule_id
130 .to_string()
131 .cmp(&right.schedule_id.to_string())
132 })
133 });
134 Ok(schedules)
135 }
136
137 pub async fn describe_schedule(
143 &self,
144 schedule_id: &ScheduleId,
145 ) -> Result<ScheduleState, EngineError> {
146 self.schedule_evaluator
147 .lock()
148 .await
149 .state(schedule_id)
150 .filter(|state| !state.is_deleted)
151 .cloned()
152 .ok_or_else(|| EngineError::ScheduleNotFound {
153 schedule_id: schedule_id.clone(),
154 })
155 }
156
157 async fn create_schedule_inner(
158 &self,
159 schedule_id: ScheduleId,
160 config: ScheduleConfig,
161 recorded_at: DateTime<Utc>,
162 ) -> Result<ScheduleId, EngineError> {
163 self.schedule_recorder
164 .lock()
165 .await
166 .record_schedule_created(recorded_at, schedule_id.clone(), config.clone())
167 .await?;
168
169 let state = ScheduleState::created(schedule_id.clone(), config, recorded_at)?;
170 let mut evaluator = self.schedule_evaluator.lock().await;
171 evaluator.upsert_state(state);
172 evaluator.arm_active_schedule(&schedule_id).await?;
173 Ok(schedule_id)
174 }
175
176 async fn update_schedule_inner(
177 &self,
178 schedule_id: &ScheduleId,
179 config: ScheduleConfig,
180 ) -> Result<(), EngineError> {
181 self.ensure_schedule_exists(schedule_id).await?;
182 let recorded_at = Utc::now();
183 self.schedule_recorder
184 .lock()
185 .await
186 .record_schedule_updated(recorded_at, schedule_id.clone(), config.clone())
187 .await?;
188 let event = Event::ScheduleUpdated {
189 envelope: schedule_event_envelope(recorded_at),
190 schedule_id: schedule_id.clone(),
191 config,
192 };
193 self.apply_schedule_event(schedule_id, &event, true).await
194 }
195
196 async fn pause_schedule_inner(&self, schedule_id: &ScheduleId) -> Result<(), EngineError> {
197 self.ensure_schedule_exists(schedule_id).await?;
198 let recorded_at = Utc::now();
199 self.schedule_recorder
200 .lock()
201 .await
202 .record_schedule_paused(recorded_at, schedule_id.clone())
203 .await?;
204 let event = Event::SchedulePaused {
205 envelope: schedule_event_envelope(recorded_at),
206 schedule_id: schedule_id.clone(),
207 };
208 self.apply_schedule_event(schedule_id, &event, false).await
209 }
210
211 async fn resume_schedule_inner(&self, schedule_id: &ScheduleId) -> Result<(), EngineError> {
212 self.ensure_schedule_exists(schedule_id).await?;
213 let recorded_at = Utc::now();
214 self.schedule_recorder
215 .lock()
216 .await
217 .record_schedule_resumed(recorded_at, schedule_id.clone())
218 .await?;
219 let event = Event::ScheduleResumed {
220 envelope: schedule_event_envelope(recorded_at),
221 schedule_id: schedule_id.clone(),
222 };
223 self.apply_schedule_event(schedule_id, &event, true).await
224 }
225
226 async fn delete_schedule_inner(&self, schedule_id: &ScheduleId) -> Result<(), EngineError> {
227 self.ensure_schedule_exists(schedule_id).await?;
228 let recorded_at = Utc::now();
229 self.schedule_recorder
230 .lock()
231 .await
232 .record_schedule_deleted(recorded_at, schedule_id.clone())
233 .await?;
234 let event = Event::ScheduleDeleted {
235 envelope: schedule_event_envelope(recorded_at),
236 schedule_id: schedule_id.clone(),
237 };
238 self.apply_schedule_event(schedule_id, &event, false).await
239 }
240
241 async fn ensure_schedule_exists(&self, schedule_id: &ScheduleId) -> Result<(), EngineError> {
242 self.schedule_evaluator
243 .lock()
244 .await
245 .state(schedule_id)
246 .filter(|state| !state.is_deleted)
247 .map(|_| ())
248 .ok_or_else(|| EngineError::ScheduleNotFound {
249 schedule_id: schedule_id.clone(),
250 })
251 }
252
253 async fn apply_schedule_event(
254 &self,
255 schedule_id: &ScheduleId,
256 event: &Event,
257 should_arm: bool,
258 ) -> Result<(), EngineError> {
259 let mut evaluator = self.schedule_evaluator.lock().await;
260 let mut state = evaluator
261 .state(schedule_id)
262 .filter(|state| !state.is_deleted)
263 .cloned()
264 .ok_or_else(|| EngineError::ScheduleNotFound {
265 schedule_id: schedule_id.clone(),
266 })?;
267 state.apply(event)?;
268 evaluator.upsert_state(state);
269 if should_arm {
270 evaluator.arm_active_schedule(schedule_id).await?;
271 }
272 Ok(())
273 }
274
275 pub async fn handle_schedule_timer_fired(
281 &self,
282 schedule_id: &ScheduleId,
283 fire_at: DateTime<Utc>,
284 ) -> Result<TimerEvaluationOutcome, EngineError> {
285 let operation = self.shutdown_gate.begin_operation()?;
286 let result = self
287 .schedule_evaluator
288 .lock()
289 .await
290 .handle_timer_fired(schedule_id, fire_at)
291 .await
292 .map_err(EngineError::from);
293 drop(operation);
294 result
295 }
296
297 pub async fn recover_schedules_on_startup(
303 &self,
304 now: DateTime<Utc>,
305 ) -> Result<(), EngineError> {
306 let source = StoreScheduleEventSource {
307 store: self.store(),
308 coordinator_workflow_id: self.schedule_coordinator_workflow_id.clone(),
309 };
310 self.schedule_evaluator
311 .lock()
312 .await
313 .recover_on_startup(&source, now)
314 .await
315 .map_err(EngineError::from)
316 }
317}
318
319#[must_use]
328pub fn schedule_coordinator_workflow_id() -> WorkflowId {
329 WorkflowId::new(uuid::Uuid::from_u128(
330 0x0000_0000_a10a_0000_0000_0000_0000_0004,
331 ))
332}
333
334pub(crate) fn schedule_coordinator_run_id() -> RunId {
335 RunId::new(uuid::Uuid::from_u128(
336 0x0000_0000_a10a_0000_0000_0000_0000_0005,
337 ))
338}
339
340pub(crate) const fn schedule_coordinator_workflow_type() -> &'static str {
341 "aion.schedule_coordinator"
342}
343
344pub(crate) fn schedule_coordinator_package_version() -> aion_core::PackageVersion {
350 aion_core::PackageVersion::new("aion:schedule-coordinator")
351}
352
353fn schedule_event_envelope(recorded_at: DateTime<Utc>) -> EventEnvelope {
354 EventEnvelope {
355 seq: 0,
356 recorded_at,
357 workflow_id: schedule_coordinator_workflow_id(),
358 }
359}
360
361pub(super) fn default_schedule_evaluator(
362 coordinator_workflow_id: WorkflowId,
363 recorder: Arc<AsyncMutex<Recorder>>,
364 deps: ScheduleRuntimeDeps,
365) -> ScheduleEvaluator {
366 let timer: Arc<dyn ScheduleTimer> = Arc::new(StoreScheduleTimer::new(
367 Arc::clone(&deps.store),
368 coordinator_workflow_id,
369 ));
370 let starter: Arc<dyn ScheduleWorkflowStarter> = Arc::new(EngineScheduleStarter { deps });
371 let canceller: Arc<dyn crate::schedule::ScheduleWorkflowCanceller> =
372 Arc::new(NoopScheduleCanceller);
373 let events: Arc<dyn ScheduleEventSink> = recorder;
374 ScheduleEvaluator::new(timer, starter, canceller, events)
375}
376
377pub(super) struct ScheduleRuntimeDeps {
378 pub(super) store: Arc<dyn EventStore>,
379 pub(super) visibility_store: Arc<dyn VisibilityStore>,
380 pub(super) runtime: Arc<RuntimeHandle>,
381 pub(super) catalog: Arc<WorkflowCatalog>,
382 pub(super) registry: Arc<Registry>,
383 pub(super) supervision: Arc<SupervisionTree>,
384 pub(super) search_attribute_schema: Arc<SearchAttributeSchema>,
385}
386
387struct EngineScheduleStarter {
388 deps: ScheduleRuntimeDeps,
389}
390
391#[async_trait]
392impl ScheduleWorkflowStarter for EngineScheduleStarter {
393 async fn start_scheduled_workflow(
394 &self,
395 workflow_type: &str,
396 input: Payload,
397 search_attributes: HashMap<String, SearchAttributeValue>,
398 ) -> Result<ScheduleExecution, ScheduleEvaluatorError> {
399 let handle = start::start_workflow_with_options(
400 StartWorkflowContext {
401 store: Arc::clone(&self.deps.store),
402 visibility_store: Arc::clone(&self.deps.visibility_store),
403 catalog: Arc::clone(&self.deps.catalog),
404 runtime: Arc::clone(&self.deps.runtime),
405 supervision: Arc::clone(&self.deps.supervision),
406 registry: Arc::clone(&self.deps.registry),
407 signal_handoff: None,
408 search_attribute_schema: Arc::clone(&self.deps.search_attribute_schema),
409 monitor_tokio_handle: tokio::runtime::Handle::current(),
410 },
411 workflow_type,
412 input,
413 start::StartWorkflowOptions {
414 search_attributes,
415 ..start::StartWorkflowOptions::default()
416 },
417 )
418 .await
419 .map_err(|error| ScheduleEvaluatorError::side_effect(error.to_string()))?;
420 Ok(ScheduleExecution::new(
421 handle.workflow_id().clone(),
422 handle.run_id().clone(),
423 ))
424 }
425}
426
427struct StoreScheduleEventSource {
428 store: Arc<dyn EventStore>,
429 coordinator_workflow_id: WorkflowId,
430}
431
432#[async_trait]
433impl ScheduleEventSource for StoreScheduleEventSource {
434 async fn schedule_events(&self) -> Result<Vec<Event>, ScheduleEvaluatorError> {
435 self.store
436 .read_history(&self.coordinator_workflow_id)
437 .await
438 .map_err(|error| ScheduleEvaluatorError::side_effect(error.to_string()))
439 }
440}
441
442#[async_trait]
443impl ScheduleEventSink for AsyncMutex<Recorder> {
444 async fn record_schedule_triggered(
445 &self,
446 schedule_id: &ScheduleId,
447 execution: &ScheduleExecution,
448 recorded_at: DateTime<Utc>,
449 ) -> Result<(), ScheduleEvaluatorError> {
450 self.lock()
451 .await
452 .record_schedule_triggered(
453 recorded_at,
454 schedule_id.clone(),
455 execution.workflow_id.clone(),
456 execution.run_id.clone(),
457 )
458 .await
459 .map_err(|error| ScheduleEvaluatorError::side_effect(error.to_string()))
460 }
461}