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