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 set_tenant_policy(
237 &self,
238 tenant_id: WorkflowTenantId,
239 policy: WorkflowTenantPolicy,
240 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
241 self.execute(move |store| block_on(store.set_tenant_policy(tenant_id, policy)))
242 }
243
244 fn set_tenant_budget_policy(
245 &self,
246 tenant_id: WorkflowTenantId,
247 policy: WorkflowTenantBudgetPolicy,
248 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
249 self.execute(move |store| block_on(store.set_tenant_budget_policy(tenant_id, policy)))
250 }
251
252 fn list_tenant_budgets(
253 &self,
254 after: Option<WorkflowTenantId>,
255 limit: WorkflowTenantListLimit,
256 ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowTenantId>, WorkflowStoreError>> {
257 self.execute(move |store| block_on(store.list_tenant_budgets(after, limit)))
258 }
259
260 fn inspect_tenant_budget(
261 &self,
262 tenant_id: WorkflowTenantId,
263 ) -> WorkflowStoreFuture<'_, Result<WorkflowTenantBudgetSnapshot, WorkflowStoreError>> {
264 self.execute(move |store| block_on(store.inspect_tenant_budget(tenant_id)))
265 }
266
267 fn list_tenant_budget_audit(
268 &self,
269 tenant_id: WorkflowTenantId,
270 after: Option<WorkflowBudgetAuditCursor>,
271 limit: WorkflowBudgetAuditLimit,
272 ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowBudgetAuditEvent>, WorkflowStoreError>> {
273 self.execute(move |store| block_on(store.list_tenant_budget_audit(tenant_id, after, limit)))
274 }
275
276 fn compact_tenant_budget_audit(
277 &self,
278 tenant_id: WorkflowTenantId,
279 through: WorkflowBudgetAuditCursor,
280 ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
281 self.execute(move |store| block_on(store.compact_tenant_budget_audit(tenant_id, through)))
282 }
283
284 fn load_or_create_tenant_budget_audit_projection(
285 &self,
286 tenant_id: WorkflowTenantId,
287 projection_id: WorkflowBudgetAuditProjectionId,
288 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditCursor, WorkflowStoreError>> {
289 self.execute(move |store| {
290 block_on(store.load_or_create_tenant_budget_audit_projection(tenant_id, projection_id))
291 })
292 }
293
294 fn advance_tenant_budget_audit_projection(
295 &self,
296 tenant_id: WorkflowTenantId,
297 projection_id: WorkflowBudgetAuditProjectionId,
298 expected: WorkflowBudgetAuditCursor,
299 next: WorkflowBudgetAuditCursor,
300 ) -> WorkflowStoreFuture<'_, Result<bool, WorkflowStoreError>> {
301 self.execute(move |store| {
302 block_on(store.advance_tenant_budget_audit_projection(
303 tenant_id,
304 projection_id,
305 expected,
306 next,
307 ))
308 })
309 }
310
311 fn claim_tenant_budget_audit_projection(
312 &self,
313 tenant_id: WorkflowTenantId,
314 projection_id: WorkflowBudgetAuditProjectionId,
315 owner: WorkerId,
316 lease: LeaseDuration,
317 ) -> WorkflowStoreFuture<
318 '_,
319 Result<Option<WorkflowBudgetAuditProjectionLease>, WorkflowStoreError>,
320 > {
321 self.execute(move |store| {
322 block_on(store.claim_tenant_budget_audit_projection(
323 tenant_id,
324 projection_id,
325 owner,
326 lease,
327 ))
328 })
329 }
330
331 fn heartbeat_tenant_budget_audit_projection(
332 &self,
333 lease: WorkflowBudgetAuditProjectionLease,
334 extension: LeaseDuration,
335 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
336 {
337 self.execute(move |store| {
338 block_on(store.heartbeat_tenant_budget_audit_projection(lease, extension))
339 })
340 }
341
342 fn advance_tenant_budget_audit_projection_lease(
343 &self,
344 lease: WorkflowBudgetAuditProjectionLease,
345 next: WorkflowBudgetAuditCursor,
346 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetAuditProjectionLease, WorkflowStoreError>>
347 {
348 self.execute(move |store| {
349 block_on(store.advance_tenant_budget_audit_projection_lease(lease, next))
350 })
351 }
352
353 fn release_tenant_budget_audit_projection(
354 &self,
355 lease: WorkflowBudgetAuditProjectionLease,
356 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
357 self.execute(move |store| block_on(store.release_tenant_budget_audit_projection(lease)))
358 }
359
360 fn reserve_budget(
361 &self,
362 lease: WorkflowLease,
363 workflow_limit: Budget,
364 baseline: Usage,
365 ) -> WorkflowStoreFuture<'_, Result<WorkflowBudgetReservationOutcome, WorkflowStoreError>> {
366 self.execute(move |store| block_on(store.reserve_budget(lease, workflow_limit, baseline)))
367 }
368
369 fn settle_budget(
370 &self,
371 lease: WorkflowLease,
372 cumulative: Usage,
373 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
374 self.execute(move |store| block_on(store.settle_budget(lease, cumulative)))
375 }
376
377 fn enqueue(
378 &self,
379 task: WorkflowTask,
380 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
381 self.execute(move |store| block_on(store.enqueue(task)))
382 }
383
384 fn claim(
385 &self,
386 worker: WorkerId,
387 lease: LeaseDuration,
388 ) -> WorkflowStoreFuture<'_, Result<Option<ClaimedWorkflow>, WorkflowStoreError>> {
389 self.execute(move |store| block_on(store.claim(worker, lease)))
390 }
391
392 fn heartbeat(
393 &self,
394 lease: WorkflowLease,
395 extension: LeaseDuration,
396 ) -> WorkflowStoreFuture<'_, Result<WorkflowLease, WorkflowStoreError>> {
397 self.execute(move |store| block_on(store.heartbeat(lease, extension)))
398 }
399
400 fn finish(
401 &self,
402 lease: WorkflowLease,
403 disposition: WorkflowDisposition,
404 ) -> WorkflowStoreFuture<'_, Result<(), WorkflowStoreError>> {
405 self.execute(move |store| block_on(store.finish(lease, disposition)))
406 }
407
408 fn publish_signal(
409 &self,
410 tenant_id: WorkflowTenantId,
411 signal: WorkflowSignal,
412 ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalOutcome, WorkflowStoreError>> {
413 self.execute(move |store| block_on(store.publish_signal(tenant_id, signal)))
414 }
415
416 fn cancel(
417 &self,
418 tenant_id: WorkflowTenantId,
419 checkpoint_id: CheckpointId,
420 ) -> WorkflowStoreFuture<'_, Result<WorkflowCancelOutcome, WorkflowStoreError>> {
421 self.execute(move |store| block_on(store.cancel(tenant_id, checkpoint_id)))
422 }
423
424 fn inspect_signal(
425 &self,
426 tenant_id: WorkflowTenantId,
427 signal_id: WorkflowSignalId,
428 ) -> WorkflowStoreFuture<'_, Result<WorkflowSignalSnapshot, WorkflowStoreError>> {
429 self.execute(move |store| block_on(store.inspect_signal(tenant_id, signal_id)))
430 }
431
432 fn compact_signals(
433 &self,
434 tenant_id: WorkflowTenantId,
435 retention: WorkflowSignalRetention,
436 ) -> WorkflowStoreFuture<'_, Result<u64, WorkflowStoreError>> {
437 self.execute(move |store| block_on(store.compact_signals(tenant_id, retention)))
438 }
439
440 fn inspect(
441 &self,
442 tenant_id: WorkflowTenantId,
443 checkpoint_id: CheckpointId,
444 ) -> WorkflowStoreFuture<'_, Result<WorkflowTaskSnapshot, WorkflowStoreError>> {
445 self.execute(move |store| block_on(store.inspect(tenant_id, checkpoint_id)))
446 }
447
448 fn list_checkpoint_history(
449 &self,
450 tenant_id: WorkflowTenantId,
451 checkpoint_id: CheckpointId,
452 after_revision: Option<u64>,
453 limit: WorkflowCheckpointHistoryLimit,
454 ) -> WorkflowStoreFuture<'_, Result<Vec<WorkflowCheckpointRevision>, WorkflowStoreError>> {
455 self.execute(move |store| {
456 block_on(store.list_checkpoint_history(tenant_id, checkpoint_id, after_revision, limit))
457 })
458 }
459
460 fn load_checkpoint_revision(
461 &self,
462 tenant_id: WorkflowTenantId,
463 checkpoint_id: CheckpointId,
464 revision: u64,
465 ) -> WorkflowStoreFuture<'_, Result<WorkflowCheckpointRevision, WorkflowStoreError>> {
466 self.execute(move |store| {
467 block_on(store.load_checkpoint_revision(tenant_id, checkpoint_id, revision))
468 })
469 }
470
471 fn fork_workflow(
472 &self,
473 tenant_id: WorkflowTenantId,
474 command: WorkflowForkCommand,
475 ) -> WorkflowStoreFuture<'_, Result<WorkflowForkOutcome, WorkflowStoreError>> {
476 self.execute(move |store| block_on(store.fork_workflow(tenant_id, command)))
477 }
478
479 fn load_checkpoint(
480 &self,
481 lease: WorkflowLease,
482 ) -> WorkflowStoreFuture<'_, Result<Checkpoint, CheckpointError>> {
483 let future = self.execute(move |store| {
484 block_on(store.load_checkpoint(lease)).map_err(checkpoint_to_workflow)
485 });
486 Box::pin(async move { future.await.map_err(workflow_to_checkpoint) })
487 }
488
489 fn compare_and_swap_checkpoint(
490 &self,
491 lease: WorkflowLease,
492 checkpoint: Checkpoint,
493 expected_revision: Option<u64>,
494 ) -> WorkflowStoreFuture<'_, Result<(), CheckpointError>> {
495 let future = self.execute(move |store| {
496 block_on(store.compare_and_swap_checkpoint(lease, checkpoint, expected_revision))
497 .map_err(checkpoint_to_workflow)
498 });
499 Box::pin(async move { future.await.map_err(workflow_to_checkpoint) })
500 }
501}