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