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#[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 pub fn new() -> Self {
45 Self::default()
46 }
47
48 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 #[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 #[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}