1use std::collections::{BTreeMap, HashMap};
4use std::sync::{Mutex, MutexGuard, PoisonError};
5
6use aion_core::{
7 Event, TimerId, WorkflowFilter, WorkflowId, WorkflowStatus, WorkflowSummary, status_from_events,
8};
9use async_trait::async_trait;
10use chrono::{DateTime, Utc};
11
12use crate::namespace::{
13 MintOutcome, NamespaceOrigin, NamespacePlacement, NamespaceRecord, NamespaceState,
14 NamespaceStore,
15};
16use crate::package::{PackageRecord, PackageRouteRecord, PackageStore};
17use crate::visibility::{ListWorkflowsFilter, VisibilityRecord, VisibilityStore};
18use crate::worker_deployment::{
19 DesiredState, PutOutcome, UndecodableWorkerDeployment, WorkerDeployment,
20 WorkerDeploymentDeleteOutcome, WorkerDeploymentListing, WorkerDeploymentPutResult,
21 WorkerDeploymentStore,
22};
23use crate::workloop::{
24 InvariantRecordSlot, InvariantStateRecord, UndecodableWorkloop, WorkloopDueProbe,
25 WorkloopListing, WorkloopRecord, WorkloopStore,
26};
27use crate::{
28 ReadableEventStore, RunSummary, StoreError, TimerEntry, TimerRetirement, WritableEventStore,
29 WriteToken,
30};
31
32#[derive(Debug, Default)]
34pub struct InMemoryStore {
35 state: Mutex<InMemoryState>,
36 namespaces: Mutex<BTreeMap<String, NamespaceRecord>>,
37 worker_deployments: Mutex<BTreeMap<String, Vec<u8>>>,
38 workloops: Mutex<BTreeMap<String, StoredWorkloopRow>>,
39 invariant_records: Mutex<BTreeMap<(String, String), Vec<u8>>>,
40}
41
42#[async_trait]
43impl VisibilityStore for InMemoryStore {
44 async fn record_visibility(&self, record: VisibilityRecord) -> Result<(), StoreError> {
45 let mut state = self.lock_state()?;
46 state
47 .visibility
48 .insert((record.workflow_id.clone(), record.run_id.clone()), record);
49 Ok(())
50 }
51
52 async fn list_workflows(
53 &self,
54 filter: ListWorkflowsFilter,
55 ) -> Result<Vec<crate::visibility::WorkflowSummary>, StoreError> {
56 let state = self.lock_state()?;
57 let mut summaries = state
58 .visibility
59 .values()
60 .cloned()
61 .map(crate::visibility::WorkflowSummary::from)
62 .filter(|summary| filter.matches(summary))
63 .collect::<Vec<_>>();
64 summaries.sort_by(|left, right| {
65 left.start_time.cmp(&right.start_time).then_with(|| {
66 left.workflow_id
67 .to_string()
68 .cmp(&right.workflow_id.to_string())
69 })
70 });
71 let offset = filter.offset.and_then(|value| usize::try_from(value).ok());
72 if let Some(offset) = offset {
73 summaries = summaries.into_iter().skip(offset).collect();
74 }
75 if let Some(limit) = filter.limit.and_then(|value| usize::try_from(value).ok()) {
76 summaries.truncate(limit);
77 }
78 Ok(summaries)
79 }
80
81 async fn count_workflows(&self, filter: ListWorkflowsFilter) -> Result<u64, StoreError> {
82 let state = self.lock_state()?;
83 Ok(state
84 .visibility
85 .values()
86 .cloned()
87 .map(crate::visibility::WorkflowSummary::from)
88 .filter(|summary| filter.matches(summary))
89 .count()
90 .try_into()
91 .unwrap_or(u64::MAX))
92 }
93}
94
95#[derive(Debug, Default)]
96struct InMemoryState {
97 histories: HashMap<WorkflowId, Vec<Event>>,
98 timers: HashMap<(WorkflowId, TimerId), TimerEntry>,
99 visibility: HashMap<(WorkflowId, aion_core::RunId), VisibilityRecord>,
100 packages: HashMap<(String, String), PackageRecord>,
101 package_routes: HashMap<String, String>,
102}
103
104impl InMemoryStore {
105 fn lock_state(&self) -> Result<MutexGuard<'_, InMemoryState>, StoreError> {
106 self.state
107 .lock()
108 .map_err(|error| StoreError::Backend(format!("in-memory store lock poisoned: {error}")))
109 }
110
111 fn lock_namespaces(&self) -> MutexGuard<'_, BTreeMap<String, NamespaceRecord>> {
117 self.namespaces
118 .lock()
119 .unwrap_or_else(PoisonError::into_inner)
120 }
121
122 fn lock_worker_deployments(&self) -> MutexGuard<'_, BTreeMap<String, Vec<u8>>> {
123 self.worker_deployments
124 .lock()
125 .unwrap_or_else(PoisonError::into_inner)
126 }
127
128 pub fn write_raw_worker_deployment(
137 &self,
138 name: &str,
139 bytes: Vec<u8>,
140 ) -> Result<(), StoreError> {
141 if name.trim().is_empty() {
142 return Err(StoreError::Serialization(
143 "worker deployment name must not be empty".to_owned(),
144 ));
145 }
146 self.lock_worker_deployments()
147 .insert(name.to_owned(), bytes);
148 Ok(())
149 }
150
151 fn lock_workloops(&self) -> MutexGuard<'_, BTreeMap<String, StoredWorkloopRow>> {
152 self.workloops
153 .lock()
154 .unwrap_or_else(PoisonError::into_inner)
155 }
156
157 fn lock_invariant_records(&self) -> MutexGuard<'_, BTreeMap<(String, String), Vec<u8>>> {
158 self.invariant_records
159 .lock()
160 .unwrap_or_else(PoisonError::into_inner)
161 }
162
163 pub fn write_raw_workloop(&self, loop_id: &str, bytes: Vec<u8>) -> Result<(), StoreError> {
170 if loop_id.trim().is_empty() {
171 return Err(StoreError::Serialization(
172 "workloop id must not be empty".to_owned(),
173 ));
174 }
175 let next_check_at =
179 WorkloopDueProbe::decode(&bytes).map_or(None, |probe| probe.next_check_at);
180 self.lock_workloops().insert(
181 loop_id.to_owned(),
182 StoredWorkloopRow {
183 next_check_at,
184 bytes,
185 },
186 );
187 Ok(())
188 }
189}
190
191#[derive(Debug)]
195struct StoredWorkloopRow {
196 next_check_at: Option<DateTime<Utc>>,
197 bytes: Vec<u8>,
198}
199
200#[async_trait]
201impl WorkloopStore for InMemoryStore {
202 async fn put_workloop(&self, record: WorkloopRecord) -> Result<(), StoreError> {
203 let bytes = record.encode()?;
204 self.lock_workloops().insert(
205 record.loop_id.to_string(),
206 StoredWorkloopRow {
207 next_check_at: record.next_check_at,
208 bytes,
209 },
210 );
211 Ok(())
212 }
213
214 async fn get_workloop(
215 &self,
216 loop_id: &WorkflowId,
217 ) -> Result<Option<WorkloopRecord>, StoreError> {
218 self.lock_workloops()
219 .get(&loop_id.to_string())
220 .map(|row| WorkloopRecord::decode(&row.bytes))
221 .transpose()
222 }
223
224 async fn list_workloops(&self) -> Result<WorkloopListing, StoreError> {
225 let mut listing = WorkloopListing::default();
226 for (loop_id, row) in self.lock_workloops().iter() {
227 match WorkloopRecord::decode(&row.bytes) {
228 Ok(record) => listing.workloops.push(record),
229 Err(error) => listing.undecodable.push(UndecodableWorkloop {
230 loop_id: loop_id.clone(),
231 error: error.to_string(),
232 }),
233 }
234 }
235 Ok(listing)
236 }
237
238 async fn due_workloops(&self, as_of: DateTime<Utc>) -> Result<Vec<WorkloopRecord>, StoreError> {
239 let mut due = Vec::new();
240 for row in self.lock_workloops().values() {
244 if row.next_check_at.is_none_or(|at| at > as_of) {
245 continue;
246 }
247 match WorkloopRecord::decode(&row.bytes) {
248 Ok(record) => due.push(record),
249 Err(error) => {
250 tracing::warn!(%error, "skipping undecodable workloop row in due sweep");
253 }
254 }
255 }
256 Ok(due)
257 }
258
259 async fn remove_workloop(&self, loop_id: &WorkflowId) -> Result<bool, StoreError> {
260 Ok(self.lock_workloops().remove(&loop_id.to_string()).is_some())
261 }
262
263 async fn put_invariant_record(
264 &self,
265 record: InvariantStateRecord,
266 prune_before: DateTime<Utc>,
267 ) -> Result<u64, StoreError> {
268 let key = (record.loop_id.to_string(), record.invariant.clone());
269 let mut slots = self.lock_invariant_records();
270 let mut slot = match slots.get(&key) {
271 Some(bytes) => InvariantRecordSlot::decode(bytes)?.rotated(record),
272 None => InvariantRecordSlot {
273 current: record,
274 previous: Vec::new(),
275 },
276 };
277 let removed = slot.prune(prune_before);
281 slots.insert(key, slot.encode()?);
282 Ok(removed)
283 }
284
285 async fn current_invariant_record(
286 &self,
287 loop_id: &WorkflowId,
288 invariant: &str,
289 ) -> Result<Option<InvariantStateRecord>, StoreError> {
290 self.lock_invariant_records()
291 .get(&(loop_id.to_string(), invariant.to_owned()))
292 .map(|bytes| InvariantRecordSlot::decode(bytes).map(|slot| slot.current))
293 .transpose()
294 }
295
296 async fn invariant_record_generations(
297 &self,
298 loop_id: &WorkflowId,
299 invariant: &str,
300 ) -> Result<Vec<InvariantStateRecord>, StoreError> {
301 self.lock_invariant_records()
302 .get(&(loop_id.to_string(), invariant.to_owned()))
303 .map(|bytes| InvariantRecordSlot::decode(bytes).map(|slot| slot.previous))
304 .transpose()
305 .map(Option::unwrap_or_default)
306 }
307}
308
309fn history_head(history: &[Event]) -> u64 {
310 history.iter().map(Event::seq).max().unwrap_or_default()
311}
312
313fn history_in_sequence_order(history: &[Event]) -> Vec<Event> {
314 let mut ordered = history.to_vec();
315 ordered.sort_by_key(Event::seq);
316 ordered
317}
318
319#[async_trait]
320impl PackageStore for InMemoryStore {
321 async fn put_package(&self, record: PackageRecord) -> Result<(), StoreError> {
322 let primary = record.workflow_type.clone();
323 self.put_package_with_routes(record, &[primary]).await
324 }
325
326 async fn put_package_with_routes(
327 &self,
328 record: PackageRecord,
329 route_workflow_types: &[String],
330 ) -> Result<(), StoreError> {
331 let mut state = self.lock_state()?;
332 for workflow_type in route_workflow_types {
333 state
334 .package_routes
335 .insert(workflow_type.clone(), record.content_hash.clone());
336 }
337 state.packages.insert(
338 (record.workflow_type.clone(), record.content_hash.clone()),
339 record,
340 );
341 Ok(())
342 }
343
344 async fn list_packages(&self) -> Result<Vec<PackageRecord>, StoreError> {
345 let state = self.lock_state()?;
346 let mut records: Vec<PackageRecord> = state.packages.values().cloned().collect();
347 records.sort_by(|left, right| {
348 left.deployed_at
349 .cmp(&right.deployed_at)
350 .then_with(|| left.workflow_type.cmp(&right.workflow_type))
351 .then_with(|| left.content_hash.cmp(&right.content_hash))
352 });
353 Ok(records)
354 }
355
356 async fn delete_package(
357 &self,
358 workflow_type: &str,
359 content_hash: &str,
360 ) -> Result<(), StoreError> {
361 let mut state = self.lock_state()?;
362 state
363 .packages
364 .remove(&(workflow_type.to_owned(), content_hash.to_owned()));
365 Ok(())
366 }
367
368 async fn put_package_route(
369 &self,
370 workflow_type: &str,
371 content_hash: &str,
372 ) -> Result<(), StoreError> {
373 let mut state = self.lock_state()?;
374 state
375 .package_routes
376 .insert(workflow_type.to_owned(), content_hash.to_owned());
377 Ok(())
378 }
379
380 async fn list_package_routes(&self) -> Result<Vec<PackageRouteRecord>, StoreError> {
381 let state = self.lock_state()?;
382 let mut routes: Vec<PackageRouteRecord> = state
383 .package_routes
384 .iter()
385 .map(|(workflow_type, content_hash)| PackageRouteRecord {
386 workflow_type: workflow_type.clone(),
387 content_hash: content_hash.clone(),
388 })
389 .collect();
390 routes.sort_by(|left, right| left.workflow_type.cmp(&right.workflow_type));
391 Ok(routes)
392 }
393}
394
395#[async_trait]
396impl NamespaceStore for InMemoryStore {
397 async fn register_namespace(
398 &self,
399 name: &str,
400 origin: NamespaceOrigin,
401 ) -> Result<MintOutcome, StoreError> {
402 let now = Utc::now();
403 let mut namespaces = self.lock_namespaces();
404 if let Some(existing) = namespaces.get_mut(name) {
405 existing.bump_last_seen(now);
406 Ok(MintOutcome::AlreadyExisted)
407 } else {
408 namespaces.insert(
409 name.to_owned(),
410 NamespaceRecord::new_minted(name, origin, now),
411 );
412 Ok(MintOutcome::Created)
413 }
414 }
415
416 async fn put_namespace(&self, record: NamespaceRecord) -> Result<MintOutcome, StoreError> {
417 let now = Utc::now();
418 let mut namespaces = self.lock_namespaces();
419 if let Some(existing) = namespaces.get_mut(&record.name) {
420 existing.bump_last_seen(now);
424 Ok(MintOutcome::AlreadyExisted)
425 } else {
426 namespaces.insert(record.name.clone(), record);
427 Ok(MintOutcome::Created)
428 }
429 }
430
431 async fn list_namespaces(&self) -> Result<Vec<NamespaceRecord>, StoreError> {
432 let namespaces = self.lock_namespaces();
433 let mut records: Vec<NamespaceRecord> = namespaces.values().cloned().collect();
434 records.sort_by(|left, right| {
435 left.created_at
436 .cmp(&right.created_at)
437 .then_with(|| left.name.cmp(&right.name))
438 });
439 Ok(records)
440 }
441
442 async fn get_namespace(&self, name: &str) -> Result<Option<NamespaceRecord>, StoreError> {
443 let namespaces = self.lock_namespaces();
444 Ok(namespaces.get(name).cloned())
445 }
446
447 async fn set_namespace_placement(
448 &self,
449 name: &str,
450 placement: NamespacePlacement,
451 ) -> Result<Option<()>, StoreError> {
452 let now = Utc::now();
453 let mut namespaces = self.lock_namespaces();
454 let Some(existing) = namespaces.get_mut(name) else {
455 return Ok(None);
458 };
459 existing.placement = placement;
460 existing.bump_last_seen(now);
461 Ok(Some(()))
462 }
463
464 async fn deprecate_namespace(&self, name: &str) -> Result<(), StoreError> {
465 let mut namespaces = self.lock_namespaces();
466 if let Some(existing) = namespaces.get_mut(name) {
467 existing.state = NamespaceState::Deprecated;
468 }
469 Ok(())
472 }
473}
474
475#[async_trait]
476impl WorkerDeploymentStore for InMemoryStore {
477 async fn put_worker_deployment(
478 &self,
479 mut record: WorkerDeployment,
480 ) -> Result<WorkerDeploymentPutResult, StoreError> {
481 let mut deployments = self.lock_worker_deployments();
482 let outcome = if let Some(previous) = deployments.get(&record.name) {
483 match WorkerDeployment::decode(previous) {
484 Ok(previous) => {
485 record = record.preserving_previous(&previous, Utc::now());
486 }
487 Err(error) => {
488 tracing::warn!(
489 deployment_key = %record.name,
490 %error,
491 "replacing undecodable prior worker-deployment bytes"
492 );
493 }
494 }
495 PutOutcome::Replaced
496 } else {
497 PutOutcome::Created
498 };
499 deployments.insert(record.name.clone(), record.encode()?);
500 Ok(WorkerDeploymentPutResult {
501 outcome,
502 deployment: record,
503 })
504 }
505
506 async fn get_worker_deployment(
507 &self,
508 name: &str,
509 ) -> Result<Option<WorkerDeployment>, StoreError> {
510 self.lock_worker_deployments()
511 .get(name)
512 .map(|bytes| WorkerDeployment::decode(bytes))
513 .transpose()
514 }
515
516 async fn list_worker_deployments(&self) -> Result<WorkerDeploymentListing, StoreError> {
517 let mut listing = WorkerDeploymentListing::default();
518 for (name, bytes) in self.lock_worker_deployments().iter() {
519 match WorkerDeployment::decode(bytes) {
520 Ok(record) => listing.deployments.push(record),
521 Err(error) => listing.undecodable.push(UndecodableWorkerDeployment {
522 name: name.clone(),
523 error: error.to_string(),
524 }),
525 }
526 }
527 Ok(listing)
528 }
529
530 async fn set_desired_state(
531 &self,
532 name: &str,
533 desired: DesiredState,
534 ) -> Result<Option<WorkerDeployment>, StoreError> {
535 let mut deployments = self.lock_worker_deployments();
536 let Some(bytes) = deployments.get(name) else {
537 return Ok(None);
538 };
539 let mut record = WorkerDeployment::decode(bytes)?;
540 record.change_desired_state(desired, Utc::now());
541 deployments.insert(name.to_owned(), record.encode()?);
542 Ok(Some(record))
543 }
544
545 async fn delete_worker_deployment(
546 &self,
547 name: &str,
548 ) -> Result<WorkerDeploymentDeleteOutcome, StoreError> {
549 let Some(bytes) = self.lock_worker_deployments().remove(name) else {
550 return Ok(WorkerDeploymentDeleteOutcome {
551 existed: false,
552 deployment: None,
553 });
554 };
555 Ok(WorkerDeploymentDeleteOutcome {
556 existed: true,
557 deployment: WorkerDeployment::decode(&bytes).ok(),
558 })
559 }
560}
561
562#[async_trait]
563impl WritableEventStore for InMemoryStore {
564 async fn append(
565 &self,
566 _token: WriteToken,
567 workflow_id: &WorkflowId,
568 events: &[Event],
569 expected_seq: u64,
570 ) -> Result<(), StoreError> {
571 let mut state = self.lock_state()?;
572 let current_head = state
573 .histories
574 .get(workflow_id)
575 .map_or(0, |history| history_head(history));
576
577 if current_head != expected_seq {
578 return Err(StoreError::SequenceConflict {
579 expected: expected_seq,
580 found: current_head,
581 });
582 }
583
584 if events.is_empty() {
585 return Ok(());
586 }
587
588 for (next_seq, event) in (expected_seq + 1..).zip(events.iter()) {
589 if event.seq() != next_seq {
590 return Err(StoreError::Backend(format!(
591 "event sequence must be contiguous: expected {next_seq}, got {}",
592 event.seq()
593 )));
594 }
595 }
596
597 state
598 .histories
599 .entry(workflow_id.clone())
600 .or_default()
601 .extend(events.iter().cloned());
602 Ok(())
603 }
604}
605
606#[async_trait]
607impl ReadableEventStore for InMemoryStore {
608 async fn read_history(&self, workflow_id: &WorkflowId) -> Result<Vec<Event>, StoreError> {
609 let state = self.lock_state()?;
610 Ok(state
611 .histories
612 .get(workflow_id)
613 .map_or_else(Vec::new, |history| history_in_sequence_order(history)))
614 }
615
616 async fn read_history_from(
617 &self,
618 workflow_id: &WorkflowId,
619 from_seq: u64,
620 ) -> Result<Vec<Event>, StoreError> {
621 let state = self.lock_state()?;
622 Ok(state
623 .histories
624 .get(workflow_id)
625 .map_or_else(Vec::new, |history| {
626 let mut events = history
627 .iter()
628 .filter(|event| event.seq() >= from_seq)
629 .cloned()
630 .collect::<Vec<_>>();
631 events.sort_by_key(Event::seq);
632 events
633 }))
634 }
635
636 async fn read_run_chain(
637 &self,
638 workflow_id: &WorkflowId,
639 ) -> Result<Vec<RunSummary>, StoreError> {
640 let state = self.lock_state()?;
641 let Some(history) = state.histories.get(workflow_id) else {
642 return Ok(Vec::new());
643 };
644
645 crate::run_chain::run_chain_from_history(history)
646 }
647
648 async fn list_workflow_ids(&self) -> Result<Vec<WorkflowId>, StoreError> {
649 let state = self.lock_state()?;
650 let mut workflow_ids = state.histories.keys().cloned().collect::<Vec<_>>();
651 workflow_ids.sort_by_key(ToString::to_string);
652 Ok(workflow_ids)
653 }
654
655 async fn list_active(&self) -> Result<Vec<WorkflowId>, StoreError> {
656 let state = self.lock_state()?;
657 let mut active = state
658 .histories
659 .iter()
660 .filter(|(_, history)| {
661 matches!(
662 status_from_events(&history_in_sequence_order(history)),
663 WorkflowStatus::Running
664 )
665 })
666 .map(|(workflow_id, _)| workflow_id.clone())
667 .collect::<Vec<_>>();
668 active.sort_by_key(ToString::to_string);
669 Ok(active)
670 }
671
672 async fn list_paused(&self) -> Result<Vec<WorkflowId>, StoreError> {
673 let state = self.lock_state()?;
674 let mut paused = state
675 .histories
676 .iter()
677 .filter(|(_, history)| {
678 matches!(
679 status_from_events(&history_in_sequence_order(history)),
680 WorkflowStatus::Paused
681 )
682 })
683 .map(|(workflow_id, _)| workflow_id.clone())
684 .collect::<Vec<_>>();
685 paused.sort_by_key(ToString::to_string);
686 Ok(paused)
687 }
688
689 async fn query(&self, filter: &WorkflowFilter) -> Result<Vec<WorkflowSummary>, StoreError> {
690 let state = self.lock_state()?;
691 let mut summaries = state
692 .histories
693 .values()
694 .filter_map(|history| {
695 WorkflowSummary::from_history(&history_in_sequence_order(history))
696 })
697 .filter(|summary| filter.matches(summary))
698 .collect::<Vec<_>>();
699 summaries.sort_by(|left, right| {
700 left.started_at.cmp(&right.started_at).then_with(|| {
701 left.workflow_id
702 .to_string()
703 .cmp(&right.workflow_id.to_string())
704 })
705 });
706 Ok(summaries)
707 }
708
709 async fn schedule_timer(
710 &self,
711 workflow_id: &WorkflowId,
712 timer_id: &TimerId,
713 fire_at: DateTime<Utc>,
714 armed_seq: u64,
715 ) -> Result<(), StoreError> {
716 let mut state = self.lock_state()?;
717 state.timers.insert(
718 (workflow_id.clone(), timer_id.clone()),
719 TimerEntry {
720 workflow_id: workflow_id.clone(),
721 timer_id: timer_id.clone(),
722 fire_at,
723 armed_seq,
724 },
725 );
726 Ok(())
727 }
728
729 async fn retire_timer(
730 &self,
731 workflow_id: &WorkflowId,
732 timer_id: &TimerId,
733 fire_at: DateTime<Utc>,
734 armed_seq: u64,
735 ) -> Result<TimerRetirement, StoreError> {
736 let mut state = self.lock_state()?;
737 let key = (workflow_id.clone(), timer_id.clone());
738 match state.timers.get(&key) {
747 Some(entry) if entry.fire_at == fire_at && entry.armed_seq == armed_seq => {
748 state.timers.remove(&key);
749 Ok(TimerRetirement::Retired)
750 }
751 Some(_) => Ok(TimerRetirement::Superseded),
752 None => Ok(TimerRetirement::Retired),
753 }
754 }
755
756 async fn expired_timers(&self, as_of: DateTime<Utc>) -> Result<Vec<TimerEntry>, StoreError> {
757 let state = self.lock_state()?;
758 let mut timers = state
759 .timers
760 .values()
761 .filter(|entry| entry.fire_at <= as_of)
762 .cloned()
763 .collect::<Vec<_>>();
764 timers.sort_by(|left, right| {
765 left.fire_at
766 .cmp(&right.fire_at)
767 .then_with(|| {
768 left.workflow_id
769 .to_string()
770 .cmp(&right.workflow_id.to_string())
771 })
772 .then_with(|| left.timer_id.to_string().cmp(&right.timer_id.to_string()))
773 });
774 Ok(timers)
775 }
776}
777
778#[cfg(test)]
779mod tests {
780 use std::sync::Arc;
781
782 use aion_core::{
783 Event, EventEnvelope, Payload, TimerId, WorkflowError, WorkflowFilter, WorkflowId,
784 WorkflowStatus,
785 };
786 use chrono::{DateTime, Utc};
787 use serde_json::json;
788 use tokio::task;
789 use uuid::Uuid;
790
791 use super::InMemoryStore;
792 use crate::{ReadableEventStore, StoreError, TimerEntry, WritableEventStore, WriteToken};
793
794 fn write_token() -> WriteToken {
795 WriteToken::recorder()
796 }
797
798 fn recorded_at(offset_seconds: i64) -> DateTime<Utc> {
799 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
800 }
801
802 fn workflow_id(value: u128) -> WorkflowId {
803 WorkflowId::new(Uuid::from_u128(value))
804 }
805
806 fn envelope(seq: u64, workflow_id: &WorkflowId) -> EventEnvelope {
807 EventEnvelope {
808 seq,
809 recorded_at: recorded_at(i64::try_from(seq).unwrap_or_default()),
810 workflow_id: workflow_id.clone(),
811 }
812 }
813
814 fn run_id(value: u128) -> aion_core::RunId {
815 aion_core::RunId::new(Uuid::from_u128(value))
816 }
817
818 fn payload(label: &str) -> Payload {
819 Payload::from_json(&json!({ "label": label })).unwrap_or_else(|error| {
820 Payload::new(
821 aion_core::ContentType::Json,
822 format!("{{\"payload_error\":\"{error}\"}}").into_bytes(),
823 )
824 })
825 }
826
827 fn workflow_started(seq: u64, workflow_id: &WorkflowId, workflow_type: &str) -> Event {
828 Event::WorkflowStarted {
829 envelope: envelope(seq, workflow_id),
830 workflow_type: workflow_type.to_owned(),
831 input: payload("input"),
832 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
833 parent_run_id: None,
834 parent_workflow_id: None,
835 package_version: aion_core::PackageVersion::new("a".repeat(64)),
836 }
837 }
838
839 fn workflow_completed(seq: u64, workflow_id: &WorkflowId) -> Event {
840 Event::WorkflowCompleted {
841 envelope: envelope(seq, workflow_id),
842 result: payload("result"),
843 }
844 }
845
846 fn workflow_failed(seq: u64, workflow_id: &WorkflowId) -> Event {
847 Event::WorkflowFailed {
848 envelope: envelope(seq, workflow_id),
849 error: WorkflowError {
850 message: String::from("failed"),
851 details: None,
852 },
853 }
854 }
855
856 #[tokio::test]
857 async fn read_history_returns_empty_for_unknown_workflow() -> Result<(), StoreError> {
858 let store = InMemoryStore::default();
859
860 assert_eq!(store.read_history(&workflow_id(1)).await?, Vec::new());
861 Ok(())
862 }
863
864 #[tokio::test]
865 async fn append_preserves_sequence_order() -> Result<(), StoreError> {
866 let store = InMemoryStore::default();
867 let workflow_id = workflow_id(1);
868 let first = workflow_started(1, &workflow_id, "checkout");
869 let second = workflow_completed(2, &workflow_id);
870
871 store
872 .append(write_token(), &workflow_id, std::slice::from_ref(&first), 0)
873 .await?;
874 store
875 .append(
876 write_token(),
877 &workflow_id,
878 std::slice::from_ref(&second),
879 1,
880 )
881 .await?;
882
883 assert_eq!(store.read_history(&workflow_id).await?, vec![first, second]);
884 Ok(())
885 }
886
887 #[tokio::test]
888 async fn list_active_returns_only_running_workflows() -> Result<(), StoreError> {
889 let store = InMemoryStore::default();
890 let running = workflow_id(1);
891 let completed = workflow_id(2);
892
893 store
894 .append(
895 write_token(),
896 &running,
897 &[workflow_started(1, &running, "checkout")],
898 0,
899 )
900 .await?;
901 store
902 .append(
903 write_token(),
904 &completed,
905 &[
906 workflow_started(1, &completed, "checkout"),
907 workflow_completed(2, &completed),
908 ],
909 0,
910 )
911 .await?;
912
913 assert_eq!(store.list_active().await?, vec![running]);
914 Ok(())
915 }
916
917 fn workflow_paused(seq: u64, workflow_id: &WorkflowId) -> Event {
918 Event::WorkflowPaused {
919 envelope: envelope(seq, workflow_id),
920 run_id: run_id(1),
921 reason: None,
922 operator: None,
923 }
924 }
925
926 #[tokio::test]
931 async fn list_paused_and_list_active_partition_by_projected_status() -> Result<(), StoreError> {
932 let store = InMemoryStore::default();
933 let running = workflow_id(1);
934 let paused = workflow_id(2);
935
936 store
937 .append(
938 write_token(),
939 &running,
940 &[workflow_started(1, &running, "checkout")],
941 0,
942 )
943 .await?;
944 store
945 .append(
946 write_token(),
947 &paused,
948 &[
949 workflow_started(1, &paused, "checkout"),
950 workflow_paused(2, &paused),
951 ],
952 0,
953 )
954 .await?;
955
956 assert_eq!(
957 store.list_active().await?,
958 vec![running],
959 "a paused run is excluded from list_active (not respawned)"
960 );
961 assert_eq!(
962 store.list_paused().await?,
963 vec![paused],
964 "list_paused returns exactly the paused run (the hold rebuild source)"
965 );
966 Ok(())
967 }
968
969 #[tokio::test]
970 async fn list_workflow_ids_returns_running_and_terminal_histories() -> Result<(), StoreError> {
971 let store = InMemoryStore::default();
972 let running = workflow_id(2);
973 let completed = workflow_id(1);
974
975 store
976 .append(
977 write_token(),
978 &running,
979 &[workflow_started(1, &running, "checkout")],
980 0,
981 )
982 .await?;
983 store
984 .append(
985 write_token(),
986 &completed,
987 &[
988 workflow_started(1, &completed, "checkout"),
989 workflow_completed(2, &completed),
990 ],
991 0,
992 )
993 .await?;
994
995 assert_eq!(store.list_workflow_ids().await?, vec![completed, running]);
996 Ok(())
997 }
998
999 #[tokio::test]
1000 async fn read_run_chain_projects_run_id_from_started_event() -> Result<(), StoreError> {
1001 let store = InMemoryStore::default();
1002 let workflow_id = workflow_id(1);
1003
1004 store
1005 .append(
1006 write_token(),
1007 &workflow_id,
1008 &[
1009 workflow_started(1, &workflow_id, "checkout"),
1010 workflow_completed(2, &workflow_id),
1011 ],
1012 0,
1013 )
1014 .await?;
1015
1016 let chain = store.read_run_chain(&workflow_id).await?;
1017
1018 assert_eq!(chain.len(), 1);
1019 assert_eq!(chain[0].run_id, run_id(1));
1021 assert_eq!(chain[0].status, WorkflowStatus::Completed);
1022 assert_eq!(chain[0].closed_at, Some(recorded_at(2)));
1023 Ok(())
1024 }
1025
1026 #[tokio::test]
1027 async fn query_uses_core_filter_semantics() -> Result<(), StoreError> {
1028 let store = InMemoryStore::default();
1029 let running_checkout = workflow_id(1);
1030 let completed_checkout = workflow_id(2);
1031 let failed_billing = workflow_id(3);
1032
1033 store
1034 .append(
1035 write_token(),
1036 &running_checkout,
1037 &[workflow_started(1, &running_checkout, "checkout")],
1038 0,
1039 )
1040 .await?;
1041 store
1042 .append(
1043 write_token(),
1044 &completed_checkout,
1045 &[
1046 workflow_started(1, &completed_checkout, "checkout"),
1047 workflow_completed(2, &completed_checkout),
1048 ],
1049 0,
1050 )
1051 .await?;
1052 store
1053 .append(
1054 write_token(),
1055 &failed_billing,
1056 &[
1057 workflow_started(1, &failed_billing, "billing"),
1058 workflow_failed(2, &failed_billing),
1059 ],
1060 0,
1061 )
1062 .await?;
1063
1064 let filter = WorkflowFilter {
1065 workflow_type: Some(String::from("checkout")),
1066 status: Some(WorkflowStatus::Completed),
1067 started_after: Some(recorded_at(1)),
1068 started_before: Some(recorded_at(1)),
1069 parent: None,
1070 };
1071 let summaries = store.query(&filter).await?;
1072
1073 assert_eq!(summaries.len(), 1);
1074 assert_eq!(summaries[0].workflow_id, completed_checkout);
1075 assert_eq!(summaries[0].status, WorkflowStatus::Completed);
1076 Ok(())
1077 }
1078
1079 #[tokio::test]
1080 async fn stale_expected_sequence_writes_nothing() -> Result<(), StoreError> {
1081 let store = InMemoryStore::default();
1082 let workflow_id = workflow_id(1);
1083 let first = workflow_started(1, &workflow_id, "checkout");
1084
1085 store
1086 .append(write_token(), &workflow_id, std::slice::from_ref(&first), 0)
1087 .await?;
1088 let conflict = store
1089 .append(
1090 write_token(),
1091 &workflow_id,
1092 &[workflow_completed(2, &workflow_id)],
1093 0,
1094 )
1095 .await;
1096
1097 assert_eq!(
1098 conflict,
1099 Err(StoreError::SequenceConflict {
1100 expected: 0,
1101 found: 1,
1102 })
1103 );
1104 assert_eq!(store.read_history(&workflow_id).await?, vec![first]);
1105 Ok(())
1106 }
1107
1108 #[tokio::test]
1109 async fn append_rejects_non_contiguous_event_sequences() -> Result<(), StoreError> {
1110 let store = InMemoryStore::default();
1111 let wf = workflow_id(1);
1112
1113 let result = store
1114 .append(
1115 write_token(),
1116 &wf,
1117 &[
1118 workflow_started(1, &wf, "checkout"),
1119 workflow_completed(5, &wf),
1120 ],
1121 0,
1122 )
1123 .await;
1124
1125 assert!(result.is_err());
1126 assert!(matches!(result, Err(StoreError::Backend(_))));
1127 assert_eq!(store.read_history(&wf).await?, Vec::new());
1128 Ok(())
1129 }
1130
1131 #[tokio::test]
1132 async fn concurrent_appends_on_same_expected_sequence_conflict_once() -> Result<(), StoreError>
1133 {
1134 let store = Arc::new(InMemoryStore::default());
1135 let workflow_id = workflow_id(1);
1136 let first_store = Arc::clone(&store);
1137 let first_workflow = workflow_id.clone();
1138 let second_store = Arc::clone(&store);
1139 let second_workflow = workflow_id.clone();
1140
1141 let first = task::spawn(async move {
1142 first_store
1143 .append(
1144 write_token(),
1145 &first_workflow,
1146 &[workflow_started(1, &first_workflow, "checkout")],
1147 0,
1148 )
1149 .await
1150 });
1151 let second = task::spawn(async move {
1152 second_store
1153 .append(
1154 write_token(),
1155 &second_workflow,
1156 &[workflow_completed(1, &second_workflow)],
1157 0,
1158 )
1159 .await
1160 });
1161
1162 let results = [
1163 first
1164 .await
1165 .map_err(|error| StoreError::Backend(format!("append task failed: {error}")))?,
1166 second
1167 .await
1168 .map_err(|error| StoreError::Backend(format!("append task failed: {error}")))?,
1169 ];
1170
1171 assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
1172 assert_eq!(
1173 results
1174 .iter()
1175 .filter(|result| matches!(
1176 result,
1177 Err(StoreError::SequenceConflict {
1178 expected: 0,
1179 found: 1
1180 })
1181 ))
1182 .count(),
1183 1
1184 );
1185 assert_eq!(store.read_history(&workflow_id).await?.len(), 1);
1186 Ok(())
1187 }
1188
1189 #[tokio::test]
1190 async fn rescheduling_same_timer_replaces_prior_fire_at() -> Result<(), StoreError> {
1191 let store = InMemoryStore::default();
1192 let workflow_id = workflow_id(1);
1193 let timer_id = TimerId::anonymous(1);
1194 let first_fire_at = recorded_at(10);
1195 let replacement_fire_at = recorded_at(30);
1196
1197 store
1198 .schedule_timer(&workflow_id, &timer_id, first_fire_at, 1)
1199 .await?;
1200 store
1201 .schedule_timer(&workflow_id, &timer_id, replacement_fire_at, 2)
1202 .await?;
1203
1204 assert_eq!(store.expired_timers(first_fire_at).await?, Vec::new());
1205 assert_eq!(
1206 store.expired_timers(replacement_fire_at).await?,
1207 vec![TimerEntry {
1208 workflow_id,
1209 timer_id,
1210 fire_at: replacement_fire_at,
1211 armed_seq: 2,
1212 }]
1213 );
1214 Ok(())
1215 }
1216
1217 #[tokio::test]
1218 async fn expired_timers_include_boundary_and_exclude_future() -> Result<(), StoreError> {
1219 let store = InMemoryStore::default();
1220 let workflow_id = workflow_id(1);
1221 let past_timer = TimerId::anonymous(1);
1222 let boundary_timer = TimerId::anonymous(2);
1223 let future_timer = TimerId::anonymous(3);
1224 let as_of = recorded_at(20);
1225
1226 store
1227 .schedule_timer(&workflow_id, &future_timer, recorded_at(30), 1)
1228 .await?;
1229 store
1230 .schedule_timer(&workflow_id, &boundary_timer, as_of, 2)
1231 .await?;
1232 store
1233 .schedule_timer(&workflow_id, &past_timer, recorded_at(10), 3)
1234 .await?;
1235
1236 assert_eq!(
1237 store.expired_timers(as_of).await?,
1238 vec![
1239 TimerEntry {
1240 workflow_id: workflow_id.clone(),
1241 timer_id: past_timer,
1242 fire_at: recorded_at(10),
1243 armed_seq: 3,
1244 },
1245 TimerEntry {
1246 workflow_id,
1247 timer_id: boundary_timer,
1248 fire_at: as_of,
1249 armed_seq: 2,
1250 },
1251 ]
1252 );
1253 Ok(())
1254 }
1255}
1256
1257#[cfg(test)]
1258mod namespace_tests {
1259 #![allow(clippy::expect_used)]
1260
1261 use super::InMemoryStore;
1262 use crate::namespace::{
1263 MintOutcome, NamespaceOrigin, NamespacePlacement, NamespaceRecord, NamespaceState,
1264 };
1265 use crate::{NamespaceStore, StoreError};
1266 use chrono::{TimeZone, Utc};
1267 use std::collections::BTreeSet;
1268
1269 fn labels(values: &[&str]) -> BTreeSet<String> {
1270 values.iter().map(|v| (*v).to_owned()).collect()
1271 }
1272
1273 #[tokio::test]
1277 async fn set_placement_updates_only_placement_and_reports_not_found() -> Result<(), StoreError>
1278 {
1279 let store = InMemoryStore::default();
1280 store
1281 .register_namespace("orders", NamespaceOrigin::Explicit)
1282 .await?;
1283 let original = store
1284 .get_namespace("orders")
1285 .await?
1286 .expect("namespace must persist");
1287 assert_eq!(original.placement, NamespacePlacement::Unplaced);
1288
1289 let placement = NamespacePlacement::Prefer {
1290 nodes: labels(&["n1", "n2"]),
1291 };
1292 assert_eq!(
1293 store
1294 .set_namespace_placement("orders", placement.clone())
1295 .await?,
1296 Some(())
1297 );
1298 let updated = store
1299 .get_namespace("orders")
1300 .await?
1301 .expect("namespace must persist");
1302 assert_eq!(updated.placement, placement);
1303 assert_eq!(updated.origin, original.origin);
1305 assert_eq!(updated.created_at, original.created_at);
1306 assert_eq!(updated.state, original.state);
1307
1308 assert_eq!(
1310 store.set_namespace_placement("orders", placement).await?,
1311 Some(())
1312 );
1313
1314 assert_eq!(
1316 store
1317 .set_namespace_placement("ghost", NamespacePlacement::Unplaced)
1318 .await?,
1319 None
1320 );
1321 assert!(store.get_namespace("ghost").await?.is_none());
1322 Ok(())
1323 }
1324
1325 #[tokio::test]
1326 async fn register_creates_if_absent_and_persists() -> Result<(), StoreError> {
1327 let store = InMemoryStore::default();
1328
1329 let outcome = store
1330 .register_namespace("orders", NamespaceOrigin::WorkerMint)
1331 .await?;
1332
1333 assert_eq!(outcome, MintOutcome::Created);
1334 let record = store
1335 .get_namespace("orders")
1336 .await?
1337 .expect("namespace must persist");
1338 assert_eq!(record.name, "orders");
1339 assert_eq!(record.origin, NamespaceOrigin::WorkerMint);
1340 assert_eq!(record.state, NamespaceState::Active);
1341 assert_eq!(record.created_at, record.last_seen);
1342 Ok(())
1343 }
1344
1345 #[tokio::test]
1346 async fn second_register_already_existed_bumps_last_seen_only() -> Result<(), StoreError> {
1347 let store = InMemoryStore::default();
1348
1349 let first = store
1350 .register_namespace("orders", NamespaceOrigin::WorkerMint)
1351 .await?;
1352 assert_eq!(first, MintOutcome::Created);
1353 let original = store
1354 .get_namespace("orders")
1355 .await?
1356 .expect("namespace must persist");
1357
1358 let second = store
1360 .register_namespace("orders", NamespaceOrigin::Explicit)
1361 .await?;
1362 assert_eq!(second, MintOutcome::AlreadyExisted);
1363
1364 let touched = store
1365 .get_namespace("orders")
1366 .await?
1367 .expect("namespace must persist");
1368 assert_eq!(touched.created_at, original.created_at);
1369 assert_eq!(touched.origin, NamespaceOrigin::WorkerMint);
1370 assert!(touched.last_seen >= original.last_seen);
1371 Ok(())
1372 }
1373
1374 #[tokio::test]
1375 async fn put_namespace_is_idempotent_on_existing_name() -> Result<(), StoreError> {
1376 let store = InMemoryStore::default();
1377 let now = Utc
1378 .with_ymd_and_hms(2026, 6, 30, 12, 0, 0)
1379 .single()
1380 .expect("valid instant");
1381
1382 let mut record = NamespaceRecord::new_minted("billing", NamespaceOrigin::Explicit, now);
1383 record.config.kind = Some("tenant".to_owned());
1384
1385 let created = store.put_namespace(record.clone()).await?;
1386 assert_eq!(created, MintOutcome::Created);
1387
1388 let mut replacement =
1391 NamespaceRecord::new_minted("billing", NamespaceOrigin::WorkerMint, now);
1392 replacement.config.kind = None;
1393 let again = store.put_namespace(replacement).await?;
1394 assert_eq!(again, MintOutcome::AlreadyExisted);
1395
1396 let stored = store
1397 .get_namespace("billing")
1398 .await?
1399 .expect("namespace must persist");
1400 assert_eq!(stored.origin, NamespaceOrigin::Explicit);
1401 assert_eq!(stored.config.kind.as_deref(), Some("tenant"));
1402 Ok(())
1403 }
1404
1405 #[tokio::test]
1406 async fn list_orders_by_created_at_then_name() -> Result<(), StoreError> {
1407 let store = InMemoryStore::default();
1408 let earlier = Utc
1409 .with_ymd_and_hms(2026, 6, 30, 12, 0, 0)
1410 .single()
1411 .expect("valid instant");
1412 let later = Utc
1413 .with_ymd_and_hms(2026, 6, 30, 13, 0, 0)
1414 .single()
1415 .expect("valid instant");
1416
1417 store
1419 .put_namespace(NamespaceRecord::new_minted(
1420 "zeta",
1421 NamespaceOrigin::Explicit,
1422 earlier,
1423 ))
1424 .await?;
1425 store
1426 .put_namespace(NamespaceRecord::new_minted(
1427 "alpha",
1428 NamespaceOrigin::Explicit,
1429 earlier,
1430 ))
1431 .await?;
1432 store
1433 .put_namespace(NamespaceRecord::new_minted(
1434 "beta",
1435 NamespaceOrigin::Explicit,
1436 later,
1437 ))
1438 .await?;
1439
1440 let listed: Vec<String> = store
1441 .list_namespaces()
1442 .await?
1443 .into_iter()
1444 .map(|record| record.name)
1445 .collect();
1446
1447 assert_eq!(listed, vec!["alpha", "zeta", "beta"]);
1448 Ok(())
1449 }
1450
1451 #[tokio::test]
1452 async fn get_returns_none_for_absent_name() -> Result<(), StoreError> {
1453 let store = InMemoryStore::default();
1454 assert!(store.get_namespace("missing").await?.is_none());
1455 Ok(())
1456 }
1457
1458 #[tokio::test]
1459 async fn deprecate_sets_state_and_is_idempotent() -> Result<(), StoreError> {
1460 let store = InMemoryStore::default();
1461 store
1462 .register_namespace("orders", NamespaceOrigin::WorkerMint)
1463 .await?;
1464
1465 store.deprecate_namespace("orders").await?;
1466 let deprecated = store
1467 .get_namespace("orders")
1468 .await?
1469 .expect("namespace must persist");
1470 assert_eq!(deprecated.state, NamespaceState::Deprecated);
1471
1472 store.deprecate_namespace("orders").await?;
1474 let still = store
1475 .get_namespace("orders")
1476 .await?
1477 .expect("namespace must persist");
1478 assert_eq!(still.state, NamespaceState::Deprecated);
1479
1480 store.deprecate_namespace("never-seen").await?;
1482 assert!(store.get_namespace("never-seen").await?.is_none());
1483 Ok(())
1484 }
1485}