Skip to main content

runifold_workflow/store/
memory.rs

1use super::{
2    Arc, BTreeMap, Budget, Checkpoint, CheckpointError, CheckpointErrorKind, CheckpointId,
3    ClaimedWorkflow, Duration, LeaseDuration, Mutex, MutexGuard, Reverse, SystemWorkflowClock,
4    Usage, WorkerId, WorkflowBudgetAuditCursor, WorkflowBudgetAuditEvent, WorkflowBudgetAuditKind,
5    WorkflowBudgetAuditLimit, WorkflowBudgetAuditProjectionId, WorkflowBudgetAuditProjectionLease,
6    WorkflowBudgetForfeitReason, WorkflowBudgetReservationOutcome, WorkflowCancelOutcome,
7    WorkflowCheckpointHistoryLimit, WorkflowCheckpointPhase, WorkflowCheckpointRevision,
8    WorkflowClock, WorkflowDisposition, WorkflowForkCommand, WorkflowForkOutcome,
9    WorkflowInterruptRequest, WorkflowLease, WorkflowLineage, WorkflowSignal, WorkflowSignalId,
10    WorkflowSignalOutcome, WorkflowSignalRetention, WorkflowSignalSnapshot, WorkflowSignalState,
11    WorkflowStore, WorkflowStoreError, WorkflowStoreErrorKind, WorkflowStoreFuture, WorkflowTask,
12    WorkflowTaskSnapshot, WorkflowTaskStatus, WorkflowTenantBudgetPolicy,
13    WorkflowTenantBudgetSnapshot, WorkflowTenantId, WorkflowTenantListLimit, WorkflowTenantPolicy,
14    WorkflowWait, WorkflowWake, decode_revision, fork_checkpoint,
15};
16
17mod budget;
18mod checkpoint;
19mod signal;
20mod task;
21
22use signal::take_buffered_signal;
23use task::{is_non_terminal, require_current_lease, require_tenant, workflow_not_found};
24
25/// Deterministic in-memory implementation of the distributed store contract.
26#[derive(Clone)]
27pub struct InMemoryWorkflowStore {
28    tasks: Arc<Mutex<BTreeMap<CheckpointId, StoredTask>>>,
29    checkpoints: Arc<Mutex<StoredCheckpoints>>,
30    signals: Arc<Mutex<BTreeMap<WorkflowSignalId, StoredSignal>>>,
31    admission: Arc<Mutex<AdmissionState>>,
32    clock: Arc<dyn WorkflowClock>,
33}
34
35impl Default for InMemoryWorkflowStore {
36    fn default() -> Self {
37        Self::with_clock(Arc::new(SystemWorkflowClock))
38    }
39}
40
41impl InMemoryWorkflowStore {
42    /// Creates a store backed by the system clock.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Creates a store with an explicit authoritative clock.
48    pub fn with_clock(clock: Arc<dyn WorkflowClock>) -> Self {
49        Self {
50            tasks: Arc::new(Mutex::new(BTreeMap::new())),
51            checkpoints: Arc::new(Mutex::new(StoredCheckpoints::default())),
52            signals: Arc::new(Mutex::new(BTreeMap::new())),
53            admission: Arc::new(Mutex::new(AdmissionState::default())),
54            clock,
55        }
56    }
57
58    fn tasks(&self) -> MutexGuard<'_, BTreeMap<CheckpointId, StoredTask>> {
59        self.tasks
60            .lock()
61            .unwrap_or_else(std::sync::PoisonError::into_inner)
62    }
63}
64
65impl std::fmt::Debug for InMemoryWorkflowStore {
66    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        formatter
68            .debug_struct("InMemoryWorkflowStore")
69            .finish_non_exhaustive()
70    }
71}
72
73#[derive(Debug, Default)]
74struct AdmissionState {
75    tenants: BTreeMap<WorkflowTenantId, StoredTenant>,
76    budgets: BTreeMap<WorkflowTenantId, StoredTenantBudget>,
77    budget_audit_projections:
78        BTreeMap<(WorkflowTenantId, WorkflowBudgetAuditProjectionId), StoredBudgetAuditProjection>,
79    next_claim_sequence: u64,
80}
81
82#[derive(Clone, Copy, Debug)]
83struct StoredTenant {
84    policy: WorkflowTenantPolicy,
85    last_claim_sequence: u64,
86}
87
88#[derive(Clone, Debug)]
89struct StoredTenantBudget {
90    policy: WorkflowTenantBudgetPolicy,
91    window_started_at_ms: u64,
92    committed: Usage,
93    reserved: Usage,
94    reservations: BTreeMap<CheckpointId, StoredBudgetReservation>,
95    next_audit_sequence: u64,
96    audit_events: Vec<StoredBudgetAuditEvent>,
97}
98
99#[derive(Clone, Copy, Debug)]
100struct StoredBudgetReservation {
101    baseline: Usage,
102    amount: Usage,
103    reserved_at_ms: u64,
104    expires_at_ms: u64,
105}
106
107#[derive(Clone, Copy, Debug)]
108struct StoredBudgetAuditEvent {
109    cursor: WorkflowBudgetAuditCursor,
110    checkpoint_id: Option<CheckpointId>,
111    occurred_at_ms: u64,
112    kind: WorkflowBudgetAuditKind,
113    usage: Usage,
114    reservation_age_ms: Option<u64>,
115    limit: Budget,
116    committed: Usage,
117    reserved: Usage,
118}
119
120#[derive(Clone, Debug, Default)]
121struct StoredBudgetAuditProjection {
122    cursor: WorkflowBudgetAuditCursor,
123    owner: Option<WorkerId>,
124    fencing_token: u64,
125    expires_at_ms: Option<u64>,
126}
127
128#[derive(Clone, Debug)]
129struct StoredTask {
130    task: WorkflowTask,
131    state: StoredState,
132    attempts: u64,
133    fencing_token: u64,
134    wake: Option<WorkflowWake>,
135    lineage: Option<WorkflowLineage>,
136    created_at_ms: u64,
137    updated_at_ms: u64,
138}
139
140#[derive(Debug, Default)]
141struct StoredCheckpoints {
142    latest: BTreeMap<CheckpointId, Checkpoint>,
143    history: BTreeMap<(CheckpointId, u64), Checkpoint>,
144}
145
146#[derive(Clone, Debug)]
147enum StoredState {
148    Queued {
149        available_at_ms: u64,
150    },
151    Leased(WorkflowLease),
152    WaitingTimer {
153        wake_at_ms: u64,
154    },
155    WaitingSignal {
156        name: crate::WorkflowSignalName,
157    },
158    WaitingSignalOrTimeout {
159        name: crate::WorkflowSignalName,
160        wake_at_ms: u64,
161    },
162    WaitingInterrupt {
163        request: WorkflowInterruptRequest,
164    },
165    Completed,
166    Failed(String),
167    Cancelled,
168}
169
170#[derive(Clone, Debug)]
171struct StoredSignal {
172    tenant_id: WorkflowTenantId,
173    signal: WorkflowSignal,
174    consumed: bool,
175    dead_lettered: bool,
176    accepted_at_ms: u64,
177}
178
179impl InMemoryWorkflowStore {
180    fn suspend(
181        &self,
182        stored: &mut StoredTask,
183        checkpoint_id: CheckpointId,
184        wait: WorkflowWait,
185        now: u64,
186    ) -> StoredState {
187        match wait {
188            WorkflowWait::Timer { delay_ms } => {
189                stored.wake = None;
190                StoredState::WaitingTimer {
191                    wake_at_ms: now.saturating_add(delay_ms),
192                }
193            }
194            WorkflowWait::Signal { name } => {
195                self.suspend_signal(stored, checkpoint_id, name, None, now)
196            }
197            WorkflowWait::SignalOrTimeout { name, timeout_ms } => {
198                self.suspend_signal(stored, checkpoint_id, name, Some(timeout_ms), now)
199            }
200            WorkflowWait::Interrupt { request } => {
201                let name = request.signal_name();
202                let mut signals = self
203                    .signals
204                    .lock()
205                    .unwrap_or_else(std::sync::PoisonError::into_inner);
206                if let Some(wake) = take_buffered_signal(&mut signals, checkpoint_id, &name) {
207                    stored.wake = Some(wake);
208                    StoredState::Queued {
209                        available_at_ms: now,
210                    }
211                } else {
212                    stored.wake = None;
213                    StoredState::WaitingInterrupt { request }
214                }
215            }
216        }
217    }
218
219    fn suspend_signal(
220        &self,
221        stored: &mut StoredTask,
222        checkpoint_id: CheckpointId,
223        name: crate::WorkflowSignalName,
224        timeout_ms: Option<u64>,
225        now: u64,
226    ) -> StoredState {
227        let mut signals = self
228            .signals
229            .lock()
230            .unwrap_or_else(std::sync::PoisonError::into_inner);
231        if let Some(wake) = take_buffered_signal(&mut signals, checkpoint_id, &name) {
232            stored.wake = Some(wake);
233            return StoredState::Queued {
234                available_at_ms: now,
235            };
236        }
237        stored.wake = None;
238        match timeout_ms {
239            Some(timeout_ms) => StoredState::WaitingSignalOrTimeout {
240                name,
241                wake_at_ms: now.saturating_add(timeout_ms),
242            },
243            None => StoredState::WaitingSignal { name },
244        }
245    }
246}
247
248impl WorkflowStore for InMemoryWorkflowStore {
249    fn set_tenant_policy(
250        &self,
251        tenant_id: WorkflowTenantId,
252        policy: WorkflowTenantPolicy,
253    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
254        self.set_tenant_policy_impl(tenant_id, policy)
255    }
256
257    fn set_tenant_budget_policy(
258        &self,
259        tenant_id: WorkflowTenantId,
260        policy: WorkflowTenantBudgetPolicy,
261    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
262        self.set_tenant_budget_policy_impl(tenant_id, policy)
263    }
264
265    fn list_tenant_budgets(
266        &self,
267        after: Option<WorkflowTenantId>,
268        limit: WorkflowTenantListLimit,
269    ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowTenantId>, WorkflowStoreError>> {
270        self.list_tenant_budgets_impl(after, limit)
271    }
272
273    fn inspect_tenant_budget(
274        &self,
275        tenant_id: WorkflowTenantId,
276    ) -> WorkflowStoreFuture<'_, Result<WorkflowTenantBudgetSnapshot, WorkflowStoreError>> {
277        self.inspect_tenant_budget_impl(tenant_id)
278    }
279
280    fn list_tenant_budget_audit(
281        &self,
282        tenant_id: WorkflowTenantId,
283        after: Option<WorkflowBudgetAuditCursor>,
284        limit: WorkflowBudgetAuditLimit,
285    ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowBudgetAuditEvent>, WorkflowStoreError>> {
286        self.list_tenant_budget_audit_impl(tenant_id, after, limit)
287    }
288
289    fn compact_tenant_budget_audit(
290        &self,
291        tenant_id: WorkflowTenantId,
292        through: WorkflowBudgetAuditCursor,
293    ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
294        self.compact_tenant_budget_audit_impl(tenant_id, through)
295    }
296
297    fn load_or_create_tenant_budget_audit_projection(
298        &self,
299        tenant_id: WorkflowTenantId,
300        projection_id: WorkflowBudgetAuditProjectionId,
301    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditCursor, WorkflowStoreError>> {
302        self.load_or_create_tenant_budget_audit_projection_impl(tenant_id, projection_id)
303    }
304
305    fn advance_tenant_budget_audit_projection(
306        &self,
307        tenant_id: WorkflowTenantId,
308        projection_id: WorkflowBudgetAuditProjectionId,
309        expected: WorkflowBudgetAuditCursor,
310        next: WorkflowBudgetAuditCursor,
311    ) -> WorkflowStoreFuture<'_, Result<bool, WorkflowStoreError>> {
312        self.advance_tenant_budget_audit_projection_impl(tenant_id, projection_id, expected, next)
313    }
314
315    fn claim_tenant_budget_audit_projection(
316        &self,
317        tenant_id: WorkflowTenantId,
318        projection_id: WorkflowBudgetAuditProjectionId,
319        owner: WorkerId,
320        lease: LeaseDuration,
321    ) -> WorkflowStoreFuture<
322        '_,
323        Result<Option<WorkflowBudgetAuditProjectionLease>, WorkflowStoreError>,
324    > {
325        self.claim_tenant_budget_audit_projection_impl(tenant_id, projection_id, owner, lease)
326    }
327
328    fn heartbeat_tenant_budget_audit_projection(
329        &self,
330        lease: WorkflowBudgetAuditProjectionLease,
331        extension: LeaseDuration,
332    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
333    {
334        self.heartbeat_tenant_budget_audit_projection_impl(lease, extension)
335    }
336
337    fn advance_tenant_budget_audit_projection_lease(
338        &self,
339        lease: WorkflowBudgetAuditProjectionLease,
340        next: WorkflowBudgetAuditCursor,
341    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
342    {
343        self.advance_tenant_budget_audit_projection_lease_impl(lease, next)
344    }
345
346    fn release_tenant_budget_audit_projection(
347        &self,
348        lease: WorkflowBudgetAuditProjectionLease,
349    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
350        self.release_tenant_budget_audit_projection_impl(lease)
351    }
352
353    fn reserve_budget(
354        &self,
355        lease: WorkflowLease,
356        workflow_limit: Budget,
357        baseline: Usage,
358    ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetReservationOutcome, WorkflowStoreError>> {
359        self.reserve_budget_impl(lease, workflow_limit, baseline)
360    }
361
362    fn settle_budget(
363        &self,
364        lease: WorkflowLease,
365        cumulative: Usage,
366    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
367        self.settle_budget_impl(lease, cumulative)
368    }
369
370    fn enqueue(
371        &self,
372        task: WorkflowTask,
373    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
374        self.enqueue_impl(task)
375    }
376
377    fn claim(
378        &self,
379        worker: WorkerId,
380        lease: LeaseDuration,
381    ) -> WorkflowStoreFuture<'_, Result<Option<ClaimedWorkflow>, WorkflowStoreError>> {
382        self.claim_impl(worker, lease)
383    }
384
385    fn heartbeat(
386        &self,
387        lease: WorkflowLease,
388        extension: LeaseDuration,
389    ) -> WorkflowStoreFuture<'_, Result<WorkflowLease, WorkflowStoreError>> {
390        self.heartbeat_impl(lease, extension)
391    }
392
393    fn finish(
394        &self,
395        lease: WorkflowLease,
396        disposition: WorkflowDisposition,
397    ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
398        self.finish_impl(lease, disposition)
399    }
400
401    fn publish_signal(
402        &self,
403        tenant_id: WorkflowTenantId,
404        signal: WorkflowSignal,
405    ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalOutcome, WorkflowStoreError>> {
406        self.publish_signal_impl(tenant_id, signal)
407    }
408
409    fn cancel(
410        &self,
411        tenant_id: WorkflowTenantId,
412        checkpoint_id: CheckpointId,
413    ) -> WorkflowStoreFuture<'_, Result<WorkflowCancelOutcome, WorkflowStoreError>> {
414        self.cancel_impl(tenant_id, checkpoint_id)
415    }
416
417    fn inspect_signal(
418        &self,
419        tenant_id: WorkflowTenantId,
420        signal_id: WorkflowSignalId,
421    ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalSnapshot, WorkflowStoreError>> {
422        self.inspect_signal_impl(tenant_id, signal_id)
423    }
424
425    fn compact_signals(
426        &self,
427        tenant_id: WorkflowTenantId,
428        retention: WorkflowSignalRetention,
429    ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
430        self.compact_signals_impl(tenant_id, retention)
431    }
432
433    fn inspect(
434        &self,
435        tenant_id: WorkflowTenantId,
436        checkpoint_id: CheckpointId,
437    ) -> WorkflowStoreFuture<'_, Result<WorkflowTaskSnapshot, WorkflowStoreError>> {
438        self.inspect_impl(tenant_id, checkpoint_id)
439    }
440
441    fn list_checkpoint_history(
442        &self,
443        tenant_id: WorkflowTenantId,
444        checkpoint_id: CheckpointId,
445        after_revision: Option<u64>,
446        limit: WorkflowCheckpointHistoryLimit,
447    ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowCheckpointRevision>, WorkflowStoreError>> {
448        self.list_checkpoint_history_impl(tenant_id, checkpoint_id, after_revision, limit)
449    }
450
451    fn load_checkpoint_revision(
452        &self,
453        tenant_id: WorkflowTenantId,
454        checkpoint_id: CheckpointId,
455        revision: u64,
456    ) -> WorkflowStoreFuture<'_, Result<WorkflowCheckpointRevision, WorkflowStoreError>> {
457        self.load_checkpoint_revision_impl(tenant_id, checkpoint_id, revision)
458    }
459
460    fn fork_workflow(
461        &self,
462        tenant_id: WorkflowTenantId,
463        command: WorkflowForkCommand,
464    ) -> WorkflowStoreFuture<'_, Result<WorkflowForkOutcome, WorkflowStoreError>> {
465        self.fork_workflow_impl(tenant_id, command)
466    }
467
468    fn load_checkpoint(
469        &self,
470        lease: WorkflowLease,
471    ) -> WorkflowStoreFuture<'_, Result<Checkpoint, CheckpointError>> {
472        self.load_checkpoint_impl(lease)
473    }
474
475    fn compare_and_swap_checkpoint(
476        &self,
477        lease: WorkflowLease,
478        checkpoint: Checkpoint,
479        expected_revision: Option<u64>,
480    ) -> WorkflowStoreFuture<'_, Result<(), CheckpointError>> {
481        self.compare_and_swap_checkpoint_impl(lease, checkpoint, expected_revision)
482    }
483}