Skip to main content

orbit_runtime/store/
mod.rs

1//! Persistence.
2//!
3//! The `execution_events` table is the source of truth. `workflow_executions`
4//! is a projection kept in the same transaction for fast listing. Every batch
5//! of events is appended atomically together with the projection update.
6//!
7//! Two backends share the schema and the guarantees:
8//!
9//! * **SQLite** (default): a single file, WAL mode, `synchronous=FULL`.
10//! * **PostgreSQL**: pass a `postgres://` URL. All statements run on one
11//!   dedicated connection thread, so the synchronous engine never blocks the
12//!   async server runtime from inside it, and the connection is re-established
13//!   automatically if it drops.
14
15mod postgres;
16mod sqlite;
17
18use crate::model::{ExecutionState, HistoryEvent};
19use orbit_ir::WorkflowIr;
20use serde::Serialize;
21use std::path::Path;
22
23pub use self::postgres::PgStore;
24pub use self::sqlite::SqliteStore;
25
26#[derive(Debug)]
27pub enum PutDefinition {
28    Created,
29    Unchanged,
30    Conflict { existing_hash: String },
31}
32
33#[derive(Debug, Clone, Serialize)]
34pub struct ExecutionSummary {
35    pub execution_id: String,
36    pub workflow: String,
37    pub version: String,
38    pub status: String,
39    pub created_at_ms: u64,
40    pub updated_at_ms: u64,
41}
42
43#[derive(Debug, Clone, Serialize)]
44pub struct WorkerRecord {
45    pub worker_id: String,
46    pub runtime: String,
47    pub sdk: Option<String>,
48    pub contracts: serde_json::Value,
49    pub last_seen_ms: u64,
50    pub registered_at_ms: u64,
51}
52
53/// A storage error from either backend.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct StoreError(pub String);
56
57impl std::fmt::Display for StoreError {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.write_str(&self.0)
60    }
61}
62
63impl std::error::Error for StoreError {}
64
65impl From<rusqlite::Error> for StoreError {
66    fn from(e: rusqlite::Error) -> Self {
67        StoreError(format!("sqlite: {e}"))
68    }
69}
70
71impl From<::postgres::Error> for StoreError {
72    fn from(e: ::postgres::Error) -> Self {
73        StoreError(format!("postgres: {e}"))
74    }
75}
76
77pub type Result<T> = std::result::Result<T, StoreError>;
78
79/// Shared schema. `BIGINT` is rewritten to `INTEGER` for SQLite.
80pub(crate) const SCHEMA: &str = r#"
81CREATE TABLE IF NOT EXISTS schema_version (version BIGINT NOT NULL);
82CREATE TABLE IF NOT EXISTS workflow_definitions (
83    name TEXT NOT NULL,
84    version TEXT NOT NULL,
85    definition_hash TEXT NOT NULL,
86    ir TEXT NOT NULL,
87    created_at_ms BIGINT NOT NULL,
88    PRIMARY KEY (name, version)
89);
90CREATE TABLE IF NOT EXISTS workflow_executions (
91    execution_id TEXT PRIMARY KEY,
92    workflow TEXT NOT NULL,
93    version TEXT NOT NULL,
94    definition_hash TEXT NOT NULL,
95    status TEXT NOT NULL,
96    created_at_ms BIGINT NOT NULL,
97    updated_at_ms BIGINT NOT NULL,
98    last_seq BIGINT NOT NULL
99);
100CREATE INDEX IF NOT EXISTS idx_executions_status ON workflow_executions(status);
101CREATE TABLE IF NOT EXISTS execution_events (
102    execution_id TEXT NOT NULL,
103    seq BIGINT NOT NULL,
104    event_type TEXT NOT NULL,
105    step TEXT,
106    payload TEXT NOT NULL,
107    timestamp_ms BIGINT NOT NULL,
108    PRIMARY KEY (execution_id, seq)
109);
110CREATE TABLE IF NOT EXISTS worker_registry (
111    worker_id TEXT PRIMARY KEY,
112    runtime TEXT NOT NULL,
113    sdk TEXT,
114    contracts TEXT NOT NULL,
115    registered_at_ms BIGINT NOT NULL,
116    last_seen_ms BIGINT NOT NULL
117);
118"#;
119
120/// The storage backend used by the engine.
121pub enum Store {
122    Sqlite(SqliteStore),
123    Postgres(PgStore),
124}
125
126macro_rules! dispatch {
127    ($self:ident, $s:ident => $call:expr) => {
128        match $self {
129            Store::Sqlite($s) => $call,
130            Store::Postgres($s) => $call,
131        }
132    };
133}
134
135impl Store {
136    /// Open a backend from a location: a `postgres://` / `postgresql://` URL,
137    /// or a SQLite file path.
138    pub fn connect(location: &str) -> Result<Store> {
139        if location.starts_with("postgres://") || location.starts_with("postgresql://") {
140            Ok(Store::Postgres(PgStore::connect(location)?))
141        } else {
142            Store::open(Path::new(location))
143        }
144    }
145
146    /// Open (or create) a SQLite database file.
147    pub fn open(path: &Path) -> Result<Store> {
148        Ok(Store::Sqlite(SqliteStore::open(path)?))
149    }
150
151    pub fn in_memory() -> Result<Store> {
152        Ok(Store::Sqlite(SqliteStore::in_memory()?))
153    }
154
155    pub fn backend(&self) -> &'static str {
156        match self {
157            Store::Sqlite(_) => "sqlite",
158            Store::Postgres(_) => "postgres",
159        }
160    }
161
162    pub fn put_definition(&self, ir: &WorkflowIr, now: u64) -> Result<PutDefinition> {
163        dispatch!(self, s => s.put_definition(ir, now))
164    }
165
166    pub fn definitions(&self) -> Result<Vec<WorkflowIr>> {
167        dispatch!(self, s => s.definitions())
168    }
169
170    pub fn execution_exists(&self, id: &str) -> Result<bool> {
171        dispatch!(self, s => s.execution_exists(id))
172    }
173
174    /// Atomically append events and update the execution projection.
175    pub fn append(&mut self, state: &ExecutionState, events: &[HistoryEvent]) -> Result<()> {
176        dispatch!(self, s => s.append(state, events))
177    }
178
179    pub fn history(&self, id: &str) -> Result<Vec<HistoryEvent>> {
180        dispatch!(self, s => s.history(id))
181    }
182
183    /// Ids of executions that have not reached a terminal status.
184    pub fn active_execution_ids(&self) -> Result<Vec<String>> {
185        dispatch!(self, s => s.active_execution_ids())
186    }
187
188    pub fn list_executions(
189        &self,
190        status: Option<&str>,
191        workflow: Option<&str>,
192        limit: u32,
193    ) -> Result<Vec<ExecutionSummary>> {
194        dispatch!(self, s => s.list_executions(status, workflow, limit))
195    }
196
197    pub fn count_by_status(&self) -> Result<Vec<(String, u64)>> {
198        dispatch!(self, s => s.count_by_status())
199    }
200
201    pub fn upsert_worker(&self, w: &WorkerRecord) -> Result<()> {
202        dispatch!(self, s => s.upsert_worker(w))
203    }
204
205    pub fn touch_worker(&self, worker_id: &str, now: u64) -> Result<()> {
206        dispatch!(self, s => s.touch_worker(worker_id, now))
207    }
208
209    pub fn workers(&self) -> Result<Vec<WorkerRecord>> {
210        dispatch!(self, s => s.workers())
211    }
212}