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