1mod schema;
4
5use std::{
6 path::Path,
7 sync::{Arc, Mutex},
8 time::Duration,
9};
10
11use futures_executor::block_on;
12use runifold_core::{
13 Budget, Checkpoint, CheckpointError, CheckpointErrorKind, CheckpointId, Usage,
14};
15use runifold_workflow::{
16 ClaimedWorkflow, InMemoryWorkflowStore, LeaseDuration, WorkerId, WorkflowBudgetAuditCursor,
17 WorkflowBudgetAuditEvent, WorkflowBudgetAuditLimit, WorkflowBudgetAuditProjectionId,
18 WorkflowBudgetAuditProjectionLease, WorkflowBudgetReservationOutcome, WorkflowCancelOutcome,
19 WorkflowCheckpointHistoryLimit, WorkflowCheckpointRevision, WorkflowClock, WorkflowDisposition,
20 WorkflowForkCommand, WorkflowForkOutcome, WorkflowLease, WorkflowSignal, WorkflowSignalId,
21 WorkflowSignalOutcome, WorkflowSignalRetention, WorkflowSignalSnapshot, WorkflowStore,
22 WorkflowStoreError, WorkflowStoreErrorKind, WorkflowStoreFuture, WorkflowTask,
23 WorkflowTaskSnapshot, WorkflowTenantBudgetPolicy, WorkflowTenantBudgetSnapshot,
24 WorkflowTenantId, WorkflowTenantListLimit, WorkflowTenantPolicy,
25};
26use rusqlite::{Connection, OptionalExtension, Transaction, TransactionBehavior, params};
27use thiserror::Error;
28
29use self::schema::{SCHEMA, SNAPSHOT_FORMAT_VERSION};
30
31#[derive(Debug, Error)]
33#[non_exhaustive]
34pub enum SqliteWorkflowStoreError {
35 #[error("sqlite workflow store initialization failed: {0}")]
37 Database(#[from] rusqlite::Error),
38}
39
40#[derive(Clone)]
47pub struct SqliteWorkflowStore {
48 connection: Arc<Mutex<Connection>>,
49}
50
51impl SqliteWorkflowStore {
52 pub fn open(path: impl AsRef<Path>) -> Result<Self, SqliteWorkflowStoreError> {
59 let connection = Connection::open(path)?;
60 connection.pragma_update(None, "journal_mode", "WAL")?;
61 Self::from_connection(connection)
62 }
63
64 pub fn open_in_memory() -> Result<Self, SqliteWorkflowStoreError> {
70 Self::from_connection(Connection::open_in_memory()?)
71 }
72
73 fn from_connection(connection: Connection) -> Result<Self, SqliteWorkflowStoreError> {
74 connection.busy_timeout(Duration::from_secs(5))?;
75 connection.pragma_update(None, "foreign_keys", true)?;
76 connection.execute_batch(SCHEMA)?;
77 Ok(Self {
78 connection: Arc::new(Mutex::new(connection)),
79 })
80 }
81
82 fn execute<T, F>(&self, operation: F) -> WorkflowStoreFuture<'_, Result<T, WorkflowStoreError>>
83 where
84 T: Send + 'static,
85 F: FnOnce(&InMemoryWorkflowStore) -> Result<T, WorkflowStoreError> + Send + 'static,
86 {
87 let connection = Arc::clone(&self.connection);
88 Box::pin(async move {
89 let runtime = tokio::runtime::Handle::try_current().map_err(|_| {
90 WorkflowStoreError::new(
91 WorkflowStoreErrorKind::Storage,
92 "SQLite workflow operations require a Tokio runtime",
93 )
94 })?;
95 runtime
96 .spawn_blocking(move || execute_transaction(&connection, operation))
97 .await
98 .map_err(|error| storage_error(format!("SQLite workflow task failed: {error}")))?
99 })
100 }
101}
102
103impl std::fmt::Debug for SqliteWorkflowStore {
104 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 formatter
106 .debug_struct("SqliteWorkflowStore")
107 .finish_non_exhaustive()
108 }
109}
110
111#[derive(Clone, Copy, Debug)]
112struct FixedClock(u64);
113
114impl WorkflowClock for FixedClock {
115 fn now_ms(&self) -> u64 {
116 self.0
117 }
118}
119
120fn execute_transaction<T, F>(
121 connection: &Mutex<Connection>,
122 operation: F,
123) -> Result<T, WorkflowStoreError>
124where
125 F: FnOnce(&InMemoryWorkflowStore) -> Result<T, WorkflowStoreError>,
126{
127 let mut connection = connection
128 .lock()
129 .unwrap_or_else(std::sync::PoisonError::into_inner);
130 let transaction = connection
131 .transaction_with_behavior(TransactionBehavior::Immediate)
132 .map_err(|error| database_error(&error))?;
133 let now = database_now_ms(&transaction)?;
134 let state = load_state(&transaction, now)?;
135 let output = operation(&state)?;
136 save_state(&transaction, &state, now)?;
137 transaction
138 .commit()
139 .map_err(|error| database_error(&error))?;
140 Ok(output)
141}
142
143fn database_now_ms(transaction: &Transaction<'_>) -> Result<u64, WorkflowStoreError> {
144 let value = transaction
145 .query_row(
146 "SELECT CAST(strftime('%s', 'now') AS INTEGER) * 1000
147 + CAST(substr(strftime('%f', 'now'), 4, 3) AS INTEGER)",
148 [],
149 |row| row.get::<_, i64>(0),
150 )
151 .map_err(|error| database_error(&error))?;
152 u64::try_from(value).map_err(|_| storage_error("SQLite returned a negative workflow clock"))
153}
154
155fn load_state(
156 transaction: &Transaction<'_>,
157 now: u64,
158) -> Result<InMemoryWorkflowStore, WorkflowStoreError> {
159 let stored = transaction
160 .query_row(
161 "SELECT format_version, state_blob
162 FROM runifold_workflow_state
163 WHERE singleton_id = 1",
164 [],
165 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, Vec<u8>>(1)?)),
166 )
167 .optional()
168 .map_err(|error| database_error(&error))?;
169 let clock: Arc<dyn WorkflowClock> = Arc::new(FixedClock(now));
170 match stored {
171 Some((format_version, encoded)) if format_version == SNAPSHOT_FORMAT_VERSION => {
172 InMemoryWorkflowStore::from_persistent_snapshot(&encoded, clock)
173 }
174 Some((format_version, _)) => Err(storage_error(format!(
175 "unsupported SQLite workflow state format version {format_version}"
176 ))),
177 None => Ok(InMemoryWorkflowStore::with_clock(clock)),
178 }
179}
180
181fn save_state(
182 transaction: &Transaction<'_>,
183 state: &InMemoryWorkflowStore,
184 now: u64,
185) -> Result<(), WorkflowStoreError> {
186 let encoded = state.export_persistent_snapshot()?;
187 let now = i64::try_from(now)
188 .map_err(|_| storage_error("workflow clock exceeds SQLite integer range"))?;
189 transaction
190 .execute(
191 "INSERT INTO runifold_workflow_state (
192 singleton_id, format_version, state_blob, updated_at_ms
193 ) VALUES (1, ?1, ?2, ?3)
194 ON CONFLICT(singleton_id) DO UPDATE SET
195 format_version = excluded.format_version,
196 state_blob = excluded.state_blob,
197 updated_at_ms = excluded.updated_at_ms",
198 params![SNAPSHOT_FORMAT_VERSION, encoded, now],
199 )
200 .map_err(|error| database_error(&error))?;
201 Ok(())
202}
203
204fn database_error(error: &rusqlite::Error) -> WorkflowStoreError {
205 storage_error(format!("SQLite workflow operation failed: {error}"))
206}
207
208fn storage_error(message: impl Into<String>) -> WorkflowStoreError {
209 WorkflowStoreError::new(WorkflowStoreErrorKind::Storage, message)
210}
211
212fn checkpoint_to_workflow(error: CheckpointError) -> WorkflowStoreError {
213 let kind = match error.kind {
214 CheckpointErrorKind::NotFound => WorkflowStoreErrorKind::NotFound,
215 CheckpointErrorKind::Conflict => WorkflowStoreErrorKind::Conflict,
216 CheckpointErrorKind::InvalidPayload => WorkflowStoreErrorKind::InvalidInput,
217 _ => WorkflowStoreErrorKind::Storage,
218 };
219 WorkflowStoreError::new(kind, error.message)
220}
221
222fn workflow_to_checkpoint(error: WorkflowStoreError) -> CheckpointError {
223 let kind = match error.kind {
224 WorkflowStoreErrorKind::NotFound => CheckpointErrorKind::NotFound,
225 WorkflowStoreErrorKind::Conflict
226 | WorkflowStoreErrorKind::LeaseLost
227 | WorkflowStoreErrorKind::AdmissionDenied
228 | WorkflowStoreErrorKind::TenantMismatch => CheckpointErrorKind::Conflict,
229 WorkflowStoreErrorKind::InvalidInput => CheckpointErrorKind::InvalidPayload,
230 _ => CheckpointErrorKind::Storage,
231 };
232 CheckpointError::new(kind, error.message)
233}
234
235impl WorkflowStore for SqliteWorkflowStore {
236 fn current_time_ms(&self) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
237 self.execute(|store| block_on(store.current_time_ms()))
238 }
239
240 fn set_tenant_policy(
241 &self,
242 tenant_id: WorkflowTenantId,
243 policy: WorkflowTenantPolicy,
244 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
245 self.execute(move |store| block_on(store.set_tenant_policy(tenant_id, policy)))
246 }
247
248 fn set_tenant_budget_policy(
249 &self,
250 tenant_id: WorkflowTenantId,
251 policy: WorkflowTenantBudgetPolicy,
252 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
253 self.execute(move |store| block_on(store.set_tenant_budget_policy(tenant_id, policy)))
254 }
255
256 fn list_tenant_budgets(
257 &self,
258 after: Option<WorkflowTenantId>,
259 limit: WorkflowTenantListLimit,
260 ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowTenantId>, WorkflowStoreError>> {
261 self.execute(move |store| block_on(store.list_tenant_budgets(after, limit)))
262 }
263
264 fn inspect_tenant_budget(
265 &self,
266 tenant_id: WorkflowTenantId,
267 ) -> WorkflowStoreFuture<'_, Result<WorkflowTenantBudgetSnapshot, WorkflowStoreError>> {
268 self.execute(move |store| block_on(store.inspect_tenant_budget(tenant_id)))
269 }
270
271 fn list_tenant_budget_audit(
272 &self,
273 tenant_id: WorkflowTenantId,
274 after: Option<WorkflowBudgetAuditCursor>,
275 limit: WorkflowBudgetAuditLimit,
276 ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowBudgetAuditEvent>, WorkflowStoreError>> {
277 self.execute(move |store| block_on(store.list_tenant_budget_audit(tenant_id, after, limit)))
278 }
279
280 fn compact_tenant_budget_audit(
281 &self,
282 tenant_id: WorkflowTenantId,
283 through: WorkflowBudgetAuditCursor,
284 ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
285 self.execute(move |store| block_on(store.compact_tenant_budget_audit(tenant_id, through)))
286 }
287
288 fn load_or_create_tenant_budget_audit_projection(
289 &self,
290 tenant_id: WorkflowTenantId,
291 projection_id: WorkflowBudgetAuditProjectionId,
292 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditCursor, WorkflowStoreError>> {
293 self.execute(move |store| {
294 block_on(store.load_or_create_tenant_budget_audit_projection(tenant_id, projection_id))
295 })
296 }
297
298 fn advance_tenant_budget_audit_projection(
299 &self,
300 tenant_id: WorkflowTenantId,
301 projection_id: WorkflowBudgetAuditProjectionId,
302 expected: WorkflowBudgetAuditCursor,
303 next: WorkflowBudgetAuditCursor,
304 ) -> WorkflowStoreFuture<'_, Result<bool, WorkflowStoreError>> {
305 self.execute(move |store| {
306 block_on(store.advance_tenant_budget_audit_projection(
307 tenant_id,
308 projection_id,
309 expected,
310 next,
311 ))
312 })
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.execute(move |store| {
326 block_on(store.claim_tenant_budget_audit_projection(
327 tenant_id,
328 projection_id,
329 owner,
330 lease,
331 ))
332 })
333 }
334
335 fn heartbeat_tenant_budget_audit_projection(
336 &self,
337 lease: WorkflowBudgetAuditProjectionLease,
338 extension: LeaseDuration,
339 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
340 {
341 self.execute(move |store| {
342 block_on(store.heartbeat_tenant_budget_audit_projection(lease, extension))
343 })
344 }
345
346 fn advance_tenant_budget_audit_projection_lease(
347 &self,
348 lease: WorkflowBudgetAuditProjectionLease,
349 next: WorkflowBudgetAuditCursor,
350 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
351 {
352 self.execute(move |store| {
353 block_on(store.advance_tenant_budget_audit_projection_lease(lease, next))
354 })
355 }
356
357 fn release_tenant_budget_audit_projection(
358 &self,
359 lease: WorkflowBudgetAuditProjectionLease,
360 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
361 self.execute(move |store| block_on(store.release_tenant_budget_audit_projection(lease)))
362 }
363
364 fn reserve_budget(
365 &self,
366 lease: WorkflowLease,
367 workflow_limit: Budget,
368 baseline: Usage,
369 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetReservationOutcome, WorkflowStoreError>> {
370 self.execute(move |store| block_on(store.reserve_budget(lease, workflow_limit, baseline)))
371 }
372
373 fn settle_budget(
374 &self,
375 lease: WorkflowLease,
376 cumulative: Usage,
377 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
378 self.execute(move |store| block_on(store.settle_budget(lease, cumulative)))
379 }
380
381 fn enqueue(
382 &self,
383 task: WorkflowTask,
384 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
385 self.execute(move |store| block_on(store.enqueue(task)))
386 }
387
388 fn claim(
389 &self,
390 worker: WorkerId,
391 lease: LeaseDuration,
392 ) -> WorkflowStoreFuture<'_, Result<Option<ClaimedWorkflow>, WorkflowStoreError>> {
393 self.execute(move |store| block_on(store.claim(worker, lease)))
394 }
395
396 fn heartbeat(
397 &self,
398 lease: WorkflowLease,
399 extension: LeaseDuration,
400 ) -> WorkflowStoreFuture<'_, Result<WorkflowLease, WorkflowStoreError>> {
401 self.execute(move |store| block_on(store.heartbeat(lease, extension)))
402 }
403
404 fn finish(
405 &self,
406 lease: WorkflowLease,
407 disposition: WorkflowDisposition,
408 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
409 self.execute(move |store| block_on(store.finish(lease, disposition)))
410 }
411
412 fn publish_signal(
413 &self,
414 tenant_id: WorkflowTenantId,
415 signal: WorkflowSignal,
416 ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalOutcome, WorkflowStoreError>> {
417 self.execute(move |store| block_on(store.publish_signal(tenant_id, signal)))
418 }
419
420 fn publish_control_signal(
421 &self,
422 tenant_id: WorkflowTenantId,
423 signal: WorkflowSignal,
424 ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalOutcome, WorkflowStoreError>> {
425 self.execute(move |store| block_on(store.publish_control_signal(tenant_id, signal)))
426 }
427
428 fn cancel(
429 &self,
430 tenant_id: WorkflowTenantId,
431 checkpoint_id: CheckpointId,
432 ) -> WorkflowStoreFuture<'_, Result<WorkflowCancelOutcome, WorkflowStoreError>> {
433 self.execute(move |store| block_on(store.cancel(tenant_id, checkpoint_id)))
434 }
435
436 fn inspect_signal(
437 &self,
438 tenant_id: WorkflowTenantId,
439 signal_id: WorkflowSignalId,
440 ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalSnapshot, WorkflowStoreError>> {
441 self.execute(move |store| block_on(store.inspect_signal(tenant_id, signal_id)))
442 }
443
444 fn load_signal_payload(
445 &self,
446 tenant_id: WorkflowTenantId,
447 signal_id: WorkflowSignalId,
448 ) -> WorkflowStoreFuture<'_, Result<serde_json::Value, WorkflowStoreError>> {
449 self.execute(move |store| block_on(store.load_signal_payload(tenant_id, signal_id)))
450 }
451
452 fn compact_signals(
453 &self,
454 tenant_id: WorkflowTenantId,
455 retention: WorkflowSignalRetention,
456 ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
457 self.execute(move |store| block_on(store.compact_signals(tenant_id, retention)))
458 }
459
460 fn inspect(
461 &self,
462 tenant_id: WorkflowTenantId,
463 checkpoint_id: CheckpointId,
464 ) -> WorkflowStoreFuture<'_, Result<WorkflowTaskSnapshot, WorkflowStoreError>> {
465 self.execute(move |store| block_on(store.inspect(tenant_id, checkpoint_id)))
466 }
467
468 fn load_task_input(
469 &self,
470 tenant_id: WorkflowTenantId,
471 checkpoint_id: CheckpointId,
472 ) -> WorkflowStoreFuture<'_, Result<serde_json::Value, WorkflowStoreError>> {
473 self.execute(move |store| block_on(store.load_task_input(tenant_id, checkpoint_id)))
474 }
475
476 fn list_checkpoint_history(
477 &self,
478 tenant_id: WorkflowTenantId,
479 checkpoint_id: CheckpointId,
480 after_revision: Option<u64>,
481 limit: WorkflowCheckpointHistoryLimit,
482 ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowCheckpointRevision>, WorkflowStoreError>> {
483 self.execute(move |store| {
484 block_on(store.list_checkpoint_history(tenant_id, checkpoint_id, after_revision, limit))
485 })
486 }
487
488 fn load_checkpoint_revision(
489 &self,
490 tenant_id: WorkflowTenantId,
491 checkpoint_id: CheckpointId,
492 revision: u64,
493 ) -> WorkflowStoreFuture<'_, Result<WorkflowCheckpointRevision, WorkflowStoreError>> {
494 self.execute(move |store| {
495 block_on(store.load_checkpoint_revision(tenant_id, checkpoint_id, revision))
496 })
497 }
498
499 fn fork_workflow(
500 &self,
501 tenant_id: WorkflowTenantId,
502 command: WorkflowForkCommand,
503 ) -> WorkflowStoreFuture<'_, Result<WorkflowForkOutcome, WorkflowStoreError>> {
504 self.execute(move |store| block_on(store.fork_workflow(tenant_id, command)))
505 }
506
507 fn load_checkpoint(
508 &self,
509 lease: WorkflowLease,
510 ) -> WorkflowStoreFuture<'_, Result<Checkpoint, CheckpointError>> {
511 let future = self.execute(move |store| {
512 block_on(store.load_checkpoint(lease)).map_err(checkpoint_to_workflow)
513 });
514 Box::pin(async move { future.await.map_err(workflow_to_checkpoint) })
515 }
516
517 fn compare_and_swap_checkpoint(
518 &self,
519 lease: WorkflowLease,
520 checkpoint: Checkpoint,
521 expected_revision: Option<u64>,
522 ) -> WorkflowStoreFuture<'_, Result<(), CheckpointError>> {
523 let future = self.execute(move |store| {
524 block_on(store.compare_and_swap_checkpoint(lease, checkpoint, expected_revision))
525 .map_err(checkpoint_to_workflow)
526 });
527 Box::pin(async move { future.await.map_err(workflow_to_checkpoint) })
528 }
529}