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