Skip to main content

runifold_workflow/store/
memory.rs

1use super::{
2    Arc, BTreeMap, Budget, Checkpoint, CheckpointError, CheckpointErrorKind, CheckpointId,
3    ClaimedWorkflow, Deserialize, Duration, LeaseDuration, Mutex, MutexGuard, Reverse, Serialize,
4    SystemWorkflowClock, Usage, WorkerId, WorkflowBudgetAuditCursor, WorkflowBudgetAuditEvent,
5    WorkflowBudgetAuditKind, WorkflowBudgetAuditLimit, WorkflowBudgetAuditProjectionId,
6    WorkflowBudgetAuditProjectionLease, WorkflowBudgetForfeitReason,
7    WorkflowBudgetReservationOutcome, WorkflowCancelOutcome, WorkflowCheckpointHistoryLimit,
8    WorkflowCheckpointPhase, WorkflowCheckpointRevision, WorkflowClock, WorkflowDisposition,
9    WorkflowForkCommand, WorkflowForkOutcome, WorkflowInterruptRequest, WorkflowLease,
10    WorkflowLineage, WorkflowSignal, WorkflowSignalId, WorkflowSignalOutcome,
11    WorkflowSignalRetention, WorkflowSignalSnapshot, WorkflowSignalState, WorkflowStore,
12    WorkflowStoreError, WorkflowStoreErrorKind, WorkflowStoreFuture, WorkflowTask,
13    WorkflowTaskSnapshot, WorkflowTaskStatus, WorkflowTenantBudgetPolicy,
14    WorkflowTenantBudgetSnapshot, WorkflowTenantId, WorkflowTenantListLimit, WorkflowTenantPolicy,
15    WorkflowWait, WorkflowWake, decode_revision, fork_checkpoint,
16};
17
18mod budget;
19mod checkpoint;
20mod signal;
21mod task;
22
23use signal::take_buffered_signal;
24use task::{is_non_terminal, require_current_lease, require_tenant, workflow_not_found};
25
26/// Deterministic in-memory implementation of the distributed store contract.
27#[derive(Clone)]
28pub struct InMemoryWorkflowStore {
29    tasks: Arc<Mutex<BTreeMap<CheckpointId, StoredTask>>>,
30    checkpoints: Arc<Mutex<StoredCheckpoints>>,
31    signals: Arc<Mutex<BTreeMap<WorkflowSignalId, StoredSignal>>>,
32    admission: Arc<Mutex<AdmissionState>>,
33    clock: Arc<dyn WorkflowClock>,
34}
35
36impl Default for InMemoryWorkflowStore {
37    fn default() -> Self {
38        Self::with_clock(Arc::new(SystemWorkflowClock))
39    }
40}
41
42impl InMemoryWorkflowStore {
43    /// Creates a store backed by the system clock.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Creates a store with an explicit authoritative clock.
49    pub fn with_clock(clock: Arc<dyn WorkflowClock>) -> Self {
50        Self {
51            tasks: Arc::new(Mutex::new(BTreeMap::new())),
52            checkpoints: Arc::new(Mutex::new(StoredCheckpoints::default())),
53            signals: Arc::new(Mutex::new(BTreeMap::new())),
54            admission: Arc::new(Mutex::new(AdmissionState::default())),
55            clock,
56        }
57    }
58
59    fn tasks(&self) -> MutexGuard<'_, BTreeMap<CheckpointId, StoredTask>> {
60        self.tasks
61            .lock()
62            .unwrap_or_else(std::sync::PoisonError::into_inner)
63    }
64}
65
66impl std::fmt::Debug for InMemoryWorkflowStore {
67    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        formatter
69            .debug_struct("InMemoryWorkflowStore")
70            .finish_non_exhaustive()
71    }
72}
73
74#[derive(Clone, Debug, Default, Deserialize, Serialize)]
75struct AdmissionState {
76    tenants: BTreeMap<WorkflowTenantId, StoredTenant>,
77    budgets: BTreeMap<WorkflowTenantId, StoredTenantBudget>,
78    budget_audit_projections:
79        BTreeMap<(WorkflowTenantId, WorkflowBudgetAuditProjectionId), StoredBudgetAuditProjection>,
80    next_claim_sequence: u64,
81}
82
83#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
84struct StoredTenant {
85    policy: WorkflowTenantPolicy,
86    last_claim_sequence: u64,
87}
88
89#[derive(Clone, Debug, Deserialize, Serialize)]
90struct StoredTenantBudget {
91    policy: WorkflowTenantBudgetPolicy,
92    window_started_at_ms: u64,
93    committed: Usage,
94    reserved: Usage,
95    reservations: BTreeMap<CheckpointId, StoredBudgetReservation>,
96    next_audit_sequence: u64,
97    audit_events: Vec<StoredBudgetAuditEvent>,
98}
99
100#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
101struct StoredBudgetReservation {
102    baseline: Usage,
103    amount: Usage,
104    reserved_at_ms: u64,
105    expires_at_ms: u64,
106}
107
108#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
109struct StoredBudgetAuditEvent {
110    cursor: WorkflowBudgetAuditCursor,
111    checkpoint_id: Option<CheckpointId>,
112    occurred_at_ms: u64,
113    kind: WorkflowBudgetAuditKind,
114    usage: Usage,
115    reservation_age_ms: Option<u64>,
116    limit: Budget,
117    committed: Usage,
118    reserved: Usage,
119}
120
121#[derive(Clone, Debug, Default, Deserialize, Serialize)]
122struct StoredBudgetAuditProjection {
123    cursor: WorkflowBudgetAuditCursor,
124    owner: Option<WorkerId>,
125    fencing_token: u64,
126    expires_at_ms: Option<u64>,
127}
128
129#[derive(Clone, Debug, Deserialize, Serialize)]
130struct StoredTask {
131    task: WorkflowTask,
132    state: StoredState,
133    attempts: u64,
134    fencing_token: u64,
135    wake: Option<WorkflowWake>,
136    lineage: Option<WorkflowLineage>,
137    created_at_ms: u64,
138    updated_at_ms: u64,
139}
140
141#[derive(Clone, Debug, Default, Deserialize, Serialize)]
142struct StoredCheckpoints {
143    latest: BTreeMap<CheckpointId, Checkpoint>,
144    history: BTreeMap<(CheckpointId, u64), Checkpoint>,
145}
146
147#[derive(Clone, Debug, Deserialize, Serialize)]
148enum StoredState {
149    Queued {
150        available_at_ms: u64,
151    },
152    Leased(WorkflowLease),
153    WaitingTimer {
154        wake_at_ms: u64,
155    },
156    WaitingSignal {
157        name: crate::WorkflowSignalName,
158    },
159    WaitingSignalOrTimeout {
160        name: crate::WorkflowSignalName,
161        wake_at_ms: u64,
162    },
163    WaitingInterrupt {
164        request: WorkflowInterruptRequest,
165    },
166    Completed,
167    Failed(String),
168    Cancelled,
169}
170
171#[derive(Clone, Debug, Deserialize, Serialize)]
172struct StoredSignal {
173    tenant_id: WorkflowTenantId,
174    signal: WorkflowSignal,
175    consumed: bool,
176    dead_lettered: bool,
177    accepted_at_ms: u64,
178}
179
180const PERSISTENT_SNAPSHOT_VERSION: u32 = 1;
181
182#[derive(Deserialize, Serialize)]
183struct PersistentWorkflowSnapshot {
184    version: u32,
185    tasks: Vec<(CheckpointId, StoredTask)>,
186    checkpoints: Vec<(CheckpointId, Checkpoint)>,
187    checkpoint_history: Vec<(CheckpointId, u64, Checkpoint)>,
188    signals: Vec<(WorkflowSignalId, StoredSignal)>,
189    tenants: Vec<(WorkflowTenantId, StoredTenant)>,
190    budgets: Vec<(WorkflowTenantId, StoredTenantBudget)>,
191    budget_audit_projections: Vec<(
192        WorkflowTenantId,
193        WorkflowBudgetAuditProjectionId,
194        StoredBudgetAuditProjection,
195    )>,
196    next_claim_sequence: u64,
197}
198
199impl InMemoryWorkflowStore {
200    /// Encodes the complete deterministic state machine for a durable adapter.
201    ///
202    /// This adapter boundary is intentionally byte-oriented so storage crates do
203    /// not depend on Runifold's private in-memory representation.
204    #[doc(hidden)]
205    pub fn export_persistent_snapshot(&self) -> Result<Vec<u8>, WorkflowStoreError> {
206        let tasks = self.tasks();
207        let checkpoints = self
208            .checkpoints
209            .lock()
210            .unwrap_or_else(std::sync::PoisonError::into_inner);
211        let signals = self
212            .signals
213            .lock()
214            .unwrap_or_else(std::sync::PoisonError::into_inner);
215        let admission = self
216            .admission
217            .lock()
218            .unwrap_or_else(std::sync::PoisonError::into_inner);
219        let snapshot = PersistentWorkflowSnapshot {
220            version: PERSISTENT_SNAPSHOT_VERSION,
221            tasks: tasks.iter().map(|(id, task)| (*id, task.clone())).collect(),
222            checkpoints: checkpoints
223                .latest
224                .iter()
225                .map(|(id, checkpoint)| (*id, checkpoint.clone()))
226                .collect(),
227            checkpoint_history: checkpoints
228                .history
229                .iter()
230                .map(|((id, revision), checkpoint)| (*id, *revision, checkpoint.clone()))
231                .collect(),
232            signals: signals
233                .iter()
234                .map(|(id, signal)| (*id, signal.clone()))
235                .collect(),
236            tenants: admission
237                .tenants
238                .iter()
239                .map(|(id, tenant)| (id.clone(), *tenant))
240                .collect(),
241            budgets: admission
242                .budgets
243                .iter()
244                .map(|(id, budget)| (id.clone(), budget.clone()))
245                .collect(),
246            budget_audit_projections: admission
247                .budget_audit_projections
248                .iter()
249                .map(|((tenant_id, projection_id), projection)| {
250                    (tenant_id.clone(), projection_id.clone(), projection.clone())
251                })
252                .collect(),
253            next_claim_sequence: admission.next_claim_sequence,
254        };
255        serde_json::to_vec(&snapshot).map_err(|error| {
256            WorkflowStoreError::new(
257                WorkflowStoreErrorKind::Storage,
258                format!("workflow snapshot encoding failed: {error}"),
259            )
260        })
261    }
262
263    /// Restores a complete deterministic state machine for a durable adapter.
264    #[doc(hidden)]
265    pub fn from_persistent_snapshot(
266        encoded: &[u8],
267        clock: Arc<dyn WorkflowClock>,
268    ) -> Result<Self, WorkflowStoreError> {
269        let snapshot: PersistentWorkflowSnapshot =
270            serde_json::from_slice(encoded).map_err(|error| {
271                WorkflowStoreError::new(
272                    WorkflowStoreErrorKind::Storage,
273                    format!("workflow snapshot decoding failed: {error}"),
274                )
275            })?;
276        if snapshot.version != PERSISTENT_SNAPSHOT_VERSION {
277            return Err(WorkflowStoreError::new(
278                WorkflowStoreErrorKind::Storage,
279                format!("unsupported workflow snapshot version {}", snapshot.version),
280            ));
281        }
282        let checkpoints = StoredCheckpoints {
283            latest: snapshot.checkpoints.into_iter().collect(),
284            history: snapshot
285                .checkpoint_history
286                .into_iter()
287                .map(|(id, revision, checkpoint)| ((id, revision), checkpoint))
288                .collect(),
289        };
290        let admission = AdmissionState {
291            tenants: snapshot.tenants.into_iter().collect(),
292            budgets: snapshot.budgets.into_iter().collect(),
293            budget_audit_projections: snapshot
294                .budget_audit_projections
295                .into_iter()
296                .map(|(tenant_id, projection_id, projection)| {
297                    ((tenant_id, projection_id), projection)
298                })
299                .collect(),
300            next_claim_sequence: snapshot.next_claim_sequence,
301        };
302        Ok(Self {
303            tasks: Arc::new(Mutex::new(snapshot.tasks.into_iter().collect())),
304            checkpoints: Arc::new(Mutex::new(checkpoints)),
305            signals: Arc::new(Mutex::new(snapshot.signals.into_iter().collect())),
306            admission: Arc::new(Mutex::new(admission)),
307            clock,
308        })
309    }
310}
311
312impl InMemoryWorkflowStore {
313    fn suspend(
314        &self,
315        stored: &mut StoredTask,
316        checkpoint_id: CheckpointId,
317        wait: WorkflowWait,
318        now: u64,
319    ) -> StoredState {
320        match wait {
321            WorkflowWait::Timer { delay_ms } => {
322                stored.wake = None;
323                StoredState::WaitingTimer {
324                    wake_at_ms: now.saturating_add(delay_ms),
325                }
326            }
327            WorkflowWait::Signal { name } => {
328                self.suspend_signal(stored, checkpoint_id, name, None, now)
329            }
330            WorkflowWait::SignalOrTimeout { name, timeout_ms } => {
331                self.suspend_signal(stored, checkpoint_id, name, Some(timeout_ms), now)
332            }
333            WorkflowWait::Interrupt { request } => {
334                let name = request.signal_name();
335                let mut signals = self
336                    .signals
337                    .lock()
338                    .unwrap_or_else(std::sync::PoisonError::into_inner);
339                if let Some(wake) = take_buffered_signal(&mut signals, checkpoint_id, &name) {
340                    stored.wake = Some(wake);
341                    StoredState::Queued {
342                        available_at_ms: now,
343                    }
344                } else {
345                    stored.wake = None;
346                    StoredState::WaitingInterrupt { request }
347                }
348            }
349        }
350    }
351
352    fn suspend_signal(
353        &self,
354        stored: &mut StoredTask,
355        checkpoint_id: CheckpointId,
356        name: crate::WorkflowSignalName,
357        timeout_ms: Option<u64>,
358        now: u64,
359    ) -> StoredState {
360        let mut signals = self
361            .signals
362            .lock()
363            .unwrap_or_else(std::sync::PoisonError::into_inner);
364        if let Some(wake) = take_buffered_signal(&mut signals, checkpoint_id, &name) {
365            stored.wake = Some(wake);
366            return StoredState::Queued {
367                available_at_ms: now,
368            };
369        }
370        stored.wake = None;
371        match timeout_ms {
372            Some(timeout_ms) => StoredState::WaitingSignalOrTimeout {
373                name,
374                wake_at_ms: now.saturating_add(timeout_ms),
375            },
376            None => StoredState::WaitingSignal { name },
377        }
378    }
379}
380
381impl WorkflowStore for InMemoryWorkflowStore {
382    fn set_tenant_policy(
383        &self,
384        tenant_id: WorkflowTenantId,
385        policy: WorkflowTenantPolicy,
386    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
387        self.set_tenant_policy_impl(tenant_id, policy)
388    }
389
390    fn set_tenant_budget_policy(
391        &self,
392        tenant_id: WorkflowTenantId,
393        policy: WorkflowTenantBudgetPolicy,
394    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
395        self.set_tenant_budget_policy_impl(tenant_id, policy)
396    }
397
398    fn list_tenant_budgets(
399        &self,
400        after: Option<WorkflowTenantId>,
401        limit: WorkflowTenantListLimit,
402    ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowTenantId>, WorkflowStoreError>> {
403        self.list_tenant_budgets_impl(after, limit)
404    }
405
406    fn inspect_tenant_budget(
407        &self,
408        tenant_id: WorkflowTenantId,
409    ) -> WorkflowStoreFuture<'_, Result<WorkflowTenantBudgetSnapshot, WorkflowStoreError>> {
410        self.inspect_tenant_budget_impl(tenant_id)
411    }
412
413    fn list_tenant_budget_audit(
414        &self,
415        tenant_id: WorkflowTenantId,
416        after: Option<WorkflowBudgetAuditCursor>,
417        limit: WorkflowBudgetAuditLimit,
418    ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowBudgetAuditEvent>, WorkflowStoreError>> {
419        self.list_tenant_budget_audit_impl(tenant_id, after, limit)
420    }
421
422    fn compact_tenant_budget_audit(
423        &self,
424        tenant_id: WorkflowTenantId,
425        through: WorkflowBudgetAuditCursor,
426    ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
427        self.compact_tenant_budget_audit_impl(tenant_id, through)
428    }
429
430    fn load_or_create_tenant_budget_audit_projection(
431        &self,
432        tenant_id: WorkflowTenantId,
433        projection_id: WorkflowBudgetAuditProjectionId,
434    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditCursor, WorkflowStoreError>> {
435        self.load_or_create_tenant_budget_audit_projection_impl(tenant_id, projection_id)
436    }
437
438    fn advance_tenant_budget_audit_projection(
439        &self,
440        tenant_id: WorkflowTenantId,
441        projection_id: WorkflowBudgetAuditProjectionId,
442        expected: WorkflowBudgetAuditCursor,
443        next: WorkflowBudgetAuditCursor,
444    ) -> WorkflowStoreFuture<'_, Result<bool, WorkflowStoreError>> {
445        self.advance_tenant_budget_audit_projection_impl(tenant_id, projection_id, expected, next)
446    }
447
448    fn claim_tenant_budget_audit_projection(
449        &self,
450        tenant_id: WorkflowTenantId,
451        projection_id: WorkflowBudgetAuditProjectionId,
452        owner: WorkerId,
453        lease: LeaseDuration,
454    ) -> WorkflowStoreFuture<
455        '_,
456        Result<Option<WorkflowBudgetAuditProjectionLease>, WorkflowStoreError>,
457    > {
458        self.claim_tenant_budget_audit_projection_impl(tenant_id, projection_id, owner, lease)
459    }
460
461    fn heartbeat_tenant_budget_audit_projection(
462        &self,
463        lease: WorkflowBudgetAuditProjectionLease,
464        extension: LeaseDuration,
465    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
466    {
467        self.heartbeat_tenant_budget_audit_projection_impl(lease, extension)
468    }
469
470    fn advance_tenant_budget_audit_projection_lease(
471        &self,
472        lease: WorkflowBudgetAuditProjectionLease,
473        next: WorkflowBudgetAuditCursor,
474    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
475    {
476        self.advance_tenant_budget_audit_projection_lease_impl(lease, next)
477    }
478
479    fn release_tenant_budget_audit_projection(
480        &self,
481        lease: WorkflowBudgetAuditProjectionLease,
482    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
483        self.release_tenant_budget_audit_projection_impl(lease)
484    }
485
486    fn reserve_budget(
487        &self,
488        lease: WorkflowLease,
489        workflow_limit: Budget,
490        baseline: Usage,
491    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetReservationOutcome, WorkflowStoreError>> {
492        self.reserve_budget_impl(lease, workflow_limit, baseline)
493    }
494
495    fn settle_budget(
496        &self,
497        lease: WorkflowLease,
498        cumulative: Usage,
499    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
500        self.settle_budget_impl(lease, cumulative)
501    }
502
503    fn enqueue(
504        &self,
505        task: WorkflowTask,
506    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
507        self.enqueue_impl(task)
508    }
509
510    fn claim(
511        &self,
512        worker: WorkerId,
513        lease: LeaseDuration,
514    ) -> WorkflowStoreFuture<'_, Result<Option<ClaimedWorkflow>, WorkflowStoreError>> {
515        self.claim_impl(worker, lease)
516    }
517
518    fn heartbeat(
519        &self,
520        lease: WorkflowLease,
521        extension: LeaseDuration,
522    ) -> WorkflowStoreFuture<'_, Result<WorkflowLease, WorkflowStoreError>> {
523        self.heartbeat_impl(lease, extension)
524    }
525
526    fn finish(
527        &self,
528        lease: WorkflowLease,
529        disposition: WorkflowDisposition,
530    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
531        self.finish_impl(lease, disposition)
532    }
533
534    fn publish_signal(
535        &self,
536        tenant_id: WorkflowTenantId,
537        signal: WorkflowSignal,
538    ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalOutcome, WorkflowStoreError>> {
539        self.publish_signal_impl(tenant_id, signal)
540    }
541
542    fn cancel(
543        &self,
544        tenant_id: WorkflowTenantId,
545        checkpoint_id: CheckpointId,
546    ) -> WorkflowStoreFuture<'_, Result<WorkflowCancelOutcome, WorkflowStoreError>> {
547        self.cancel_impl(tenant_id, checkpoint_id)
548    }
549
550    fn inspect_signal(
551        &self,
552        tenant_id: WorkflowTenantId,
553        signal_id: WorkflowSignalId,
554    ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalSnapshot, WorkflowStoreError>> {
555        self.inspect_signal_impl(tenant_id, signal_id)
556    }
557
558    fn compact_signals(
559        &self,
560        tenant_id: WorkflowTenantId,
561        retention: WorkflowSignalRetention,
562    ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
563        self.compact_signals_impl(tenant_id, retention)
564    }
565
566    fn inspect(
567        &self,
568        tenant_id: WorkflowTenantId,
569        checkpoint_id: CheckpointId,
570    ) -> WorkflowStoreFuture<'_, Result<WorkflowTaskSnapshot, WorkflowStoreError>> {
571        self.inspect_impl(tenant_id, checkpoint_id)
572    }
573
574    fn list_checkpoint_history(
575        &self,
576        tenant_id: WorkflowTenantId,
577        checkpoint_id: CheckpointId,
578        after_revision: Option<u64>,
579        limit: WorkflowCheckpointHistoryLimit,
580    ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowCheckpointRevision>, WorkflowStoreError>> {
581        self.list_checkpoint_history_impl(tenant_id, checkpoint_id, after_revision, limit)
582    }
583
584    fn load_checkpoint_revision(
585        &self,
586        tenant_id: WorkflowTenantId,
587        checkpoint_id: CheckpointId,
588        revision: u64,
589    ) -> WorkflowStoreFuture<'_, Result<WorkflowCheckpointRevision, WorkflowStoreError>> {
590        self.load_checkpoint_revision_impl(tenant_id, checkpoint_id, revision)
591    }
592
593    fn fork_workflow(
594        &self,
595        tenant_id: WorkflowTenantId,
596        command: WorkflowForkCommand,
597    ) -> WorkflowStoreFuture<'_, Result<WorkflowForkOutcome, WorkflowStoreError>> {
598        self.fork_workflow_impl(tenant_id, command)
599    }
600
601    fn load_checkpoint(
602        &self,
603        lease: WorkflowLease,
604    ) -> WorkflowStoreFuture<'_, Result<Checkpoint, CheckpointError>> {
605        self.load_checkpoint_impl(lease)
606    }
607
608    fn compare_and_swap_checkpoint(
609        &self,
610        lease: WorkflowLease,
611        checkpoint: Checkpoint,
612        expected_revision: Option<u64>,
613    ) -> WorkflowStoreFuture<'_, Result<(), CheckpointError>> {
614        self.compare_and_swap_checkpoint_impl(lease, checkpoint, expected_revision)
615    }
616}