1use anyhow::Result;
2use sqlx::SqlitePool;
3use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
4
5use crate::store::{NamespaceRecord, NamespaceStats, QueueStats, WorkflowStore};
6use crate::types::*;
7
8const SCHEMA: &str = r#"
21CREATE TABLE IF NOT EXISTS workflow.namespaces (
22 name TEXT PRIMARY KEY,
23 created_at REAL NOT NULL
24);
25
26INSERT OR IGNORE INTO workflow.namespaces (name, created_at)
27 VALUES ('main', strftime('%s', 'now'));
28
29CREATE TABLE IF NOT EXISTS workflow.workflows (
30 id TEXT PRIMARY KEY,
31 namespace TEXT NOT NULL DEFAULT 'main',
32 run_id TEXT NOT NULL,
33 workflow_type TEXT NOT NULL,
34 task_queue TEXT NOT NULL DEFAULT 'main',
35 status TEXT NOT NULL DEFAULT 'PENDING',
36 input TEXT,
37 result TEXT,
38 error TEXT,
39 parent_id TEXT,
40 claimed_by TEXT,
41 search_attributes TEXT,
42 archived_at REAL,
43 archive_uri TEXT,
44 -- Workflow-task dispatch (Phase 9): a workflow is "dispatchable" when
45 -- it has new events a worker needs to replay against. Set true on
46 -- start, on activity completion, on timer fire, on signal arrival.
47 -- Cleared when a worker claims the dispatch lease.
48 needs_dispatch INTEGER NOT NULL DEFAULT 0,
49 dispatch_claimed_by TEXT,
50 dispatch_last_heartbeat REAL,
51 created_at REAL NOT NULL,
52 updated_at REAL NOT NULL,
53 completed_at REAL
54);
55CREATE INDEX IF NOT EXISTS workflow.idx_wf_status_queue ON workflows(status, task_queue);
56CREATE INDEX IF NOT EXISTS workflow.idx_wf_namespace ON workflows(namespace);
57CREATE INDEX IF NOT EXISTS workflow.idx_wf_dispatch ON workflows(task_queue, needs_dispatch, dispatch_claimed_by);
58
59CREATE TABLE IF NOT EXISTS workflow.events (
60 id INTEGER PRIMARY KEY AUTOINCREMENT,
61 workflow_id TEXT NOT NULL REFERENCES workflows(id),
62 seq INTEGER NOT NULL,
63 event_type TEXT NOT NULL,
64 payload TEXT,
65 timestamp REAL NOT NULL
66);
67CREATE INDEX IF NOT EXISTS workflow.idx_wf_events_lookup ON events(workflow_id, seq);
68
69CREATE TABLE IF NOT EXISTS workflow.activities (
70 id INTEGER PRIMARY KEY AUTOINCREMENT,
71 workflow_id TEXT NOT NULL REFERENCES workflows(id),
72 seq INTEGER NOT NULL,
73 name TEXT NOT NULL,
74 task_queue TEXT NOT NULL DEFAULT 'main',
75 input TEXT,
76 status TEXT NOT NULL DEFAULT 'PENDING',
77 result TEXT,
78 error TEXT,
79 attempt INTEGER NOT NULL DEFAULT 1,
80 max_attempts INTEGER NOT NULL DEFAULT 3,
81 initial_interval_secs REAL NOT NULL DEFAULT 1,
82 backoff_coefficient REAL NOT NULL DEFAULT 2,
83 start_to_close_secs REAL NOT NULL DEFAULT 300,
84 heartbeat_timeout_secs REAL,
85 claimed_by TEXT,
86 scheduled_at REAL NOT NULL,
87 started_at REAL,
88 completed_at REAL,
89 last_heartbeat REAL,
90 UNIQUE (workflow_id, seq)
91);
92CREATE INDEX IF NOT EXISTS workflow.idx_wf_act_pending ON activities(task_queue, status, scheduled_at);
93
94CREATE TABLE IF NOT EXISTS workflow.timers (
95 id INTEGER PRIMARY KEY AUTOINCREMENT,
96 workflow_id TEXT NOT NULL REFERENCES workflows(id),
97 seq INTEGER NOT NULL,
98 fire_at REAL NOT NULL,
99 fired INTEGER NOT NULL DEFAULT 0,
100 UNIQUE (workflow_id, seq)
101);
102CREATE INDEX IF NOT EXISTS workflow.idx_wf_timers_due ON timers(fire_at);
103
104CREATE TABLE IF NOT EXISTS workflow.signals (
105 id INTEGER PRIMARY KEY AUTOINCREMENT,
106 workflow_id TEXT NOT NULL REFERENCES workflows(id),
107 name TEXT NOT NULL,
108 payload TEXT,
109 consumed INTEGER NOT NULL DEFAULT 0,
110 received_at REAL NOT NULL
111);
112CREATE INDEX IF NOT EXISTS workflow.idx_wf_signals_lookup ON signals(workflow_id, name, consumed);
113
114CREATE TABLE IF NOT EXISTS workflow.schedules (
115 name TEXT NOT NULL,
116 namespace TEXT NOT NULL DEFAULT 'main',
117 workflow_type TEXT NOT NULL,
118 cron_expr TEXT NOT NULL,
119 timezone TEXT NOT NULL DEFAULT 'UTC',
120 input TEXT,
121 task_queue TEXT NOT NULL DEFAULT 'main',
122 overlap_policy TEXT NOT NULL DEFAULT 'skip',
123 paused INTEGER NOT NULL DEFAULT 0,
124 last_run_at REAL,
125 next_run_at REAL,
126 last_workflow_id TEXT,
127 created_at REAL NOT NULL,
128 PRIMARY KEY (namespace, name)
129);
130
131CREATE TABLE IF NOT EXISTS workflow.workers (
132 id TEXT PRIMARY KEY,
133 namespace TEXT NOT NULL DEFAULT 'main',
134 identity TEXT NOT NULL,
135 task_queue TEXT NOT NULL,
136 workflows TEXT,
137 activities TEXT,
138 max_concurrent_workflows INTEGER NOT NULL DEFAULT 10,
139 max_concurrent_activities INTEGER NOT NULL DEFAULT 10,
140 active_tasks INTEGER NOT NULL DEFAULT 0,
141 last_heartbeat REAL NOT NULL,
142 registered_at REAL NOT NULL
143);
144
145CREATE TABLE IF NOT EXISTS workflow.snapshots (
146 workflow_id TEXT NOT NULL REFERENCES workflows(id),
147 event_seq INTEGER NOT NULL,
148 state_json TEXT NOT NULL,
149 created_at REAL NOT NULL,
150 PRIMARY KEY (workflow_id, event_seq)
151);
152
153-- workflow.api_keys retired in plan-15 slice 3 (auth tokens come from
154-- the auth module).
155DROP TABLE IF EXISTS workflow.api_keys;
156
157CREATE TABLE IF NOT EXISTS engine.lock (
158 id INTEGER PRIMARY KEY CHECK (id = 1),
159 instance_id TEXT NOT NULL,
160 started_at REAL NOT NULL,
161 last_heartbeat REAL NOT NULL
162);
163
164CREATE TABLE IF NOT EXISTS engine.events (
165 id INTEGER PRIMARY KEY AUTOINCREMENT,
166 ts REAL NOT NULL DEFAULT (CAST(strftime('%s','now') AS REAL)),
167 namespace TEXT NOT NULL,
168 subsystem TEXT NOT NULL,
169 kind TEXT NOT NULL,
170 payload TEXT NOT NULL DEFAULT '{}'
171);
172CREATE INDEX IF NOT EXISTS engine.idx_engine_events_ns_id ON events(namespace, id);
173CREATE INDEX IF NOT EXISTS engine.idx_engine_events_ts_prune ON events(ts);
174"#;
175
176const LOCK_STALE_SECS: f64 = 60.0;
179const LOCK_HEARTBEAT_SECS: u64 = 15;
181
182#[derive(Clone)]
189pub struct SqliteStore {
190 pool: SqlitePool,
191 instance_id: String,
192}
193
194async fn build_default_pool(url: &str) -> Result<SqlitePool> {
207 use std::str::FromStr;
208 use std::sync::atomic::{AtomicU64, Ordering};
209
210 static SEQ: AtomicU64 = AtomicU64::new(0);
211 let suffix = format!(
212 "{}_{}",
213 std::process::id(),
214 SEQ.fetch_add(1, Ordering::Relaxed)
215 );
216 let engine_alias = format!(
217 "file:assay_engine_{suffix}?mode=memory&cache=shared"
218 );
219 let workflow_alias = format!(
220 "file:assay_workflow_{suffix}?mode=memory&cache=shared"
221 );
222
223 let opts = SqliteConnectOptions::from_str(url)?
224 .create_if_missing(true);
225
226 let pool = SqlitePoolOptions::new()
227 .max_connections(1)
228 .after_connect(move |conn, _meta| {
229 let engine_alias = engine_alias.clone();
230 let workflow_alias = workflow_alias.clone();
231 Box::pin(async move {
232 use sqlx::Executor;
233 conn.execute(
234 format!("ATTACH DATABASE '{engine_alias}' AS engine").as_str(),
235 )
236 .await?;
237 conn.execute(
238 format!("ATTACH DATABASE '{workflow_alias}' AS workflow").as_str(),
239 )
240 .await?;
241 Ok(())
242 })
243 })
244 .connect_with(opts)
245 .await?;
246 Ok(pool)
247}
248
249impl SqliteStore {
250 pub async fn new(url: &str) -> Result<Self> {
256 let pool = build_default_pool(url).await?;
257 Self::from_attached_pool(pool).await
258 }
259
260 pub async fn from_attached_pool(pool: SqlitePool) -> Result<Self> {
265 let instance_id = format!("assay-{:016x}", {
266 use std::collections::hash_map::DefaultHasher;
267 use std::hash::{Hash, Hasher};
268 let mut h = DefaultHasher::new();
269 std::time::SystemTime::now().hash(&mut h);
270 std::process::id().hash(&mut h);
271 h.finish()
272 });
273 let store = Self { pool, instance_id };
274 store.migrate().await?;
275 Ok(store)
276 }
277
278 pub async fn from_pool(pool: SqlitePool) -> Result<Self> {
284 Self::from_attached_pool(pool).await
285 }
286
287 pub fn pool(&self) -> &SqlitePool {
290 &self.pool
291 }
292
293 pub async fn acquire_engine_lock(&self) -> Result<()> {
296 let now = timestamp_now();
297
298 let result = sqlx::query(
300 "INSERT INTO engine.lock (id, instance_id, started_at, last_heartbeat) VALUES (1, ?, ?, ?)",
301 )
302 .bind(&self.instance_id)
303 .bind(now)
304 .bind(now)
305 .execute(&self.pool)
306 .await;
307
308 match result {
309 Ok(_) => Ok(()),
310 Err(_) => {
311 let row: Option<(String, f64)> = sqlx::query_as(
313 "SELECT instance_id, last_heartbeat FROM engine.lock WHERE id = 1",
314 )
315 .fetch_optional(&self.pool)
316 .await?;
317
318 if let Some((existing_id, last_hb)) = row {
319 if now - last_hb > LOCK_STALE_SECS {
320 sqlx::query(
322 "UPDATE engine.lock SET instance_id = ?, started_at = ?, last_heartbeat = ? WHERE id = 1",
323 )
324 .bind(&self.instance_id)
325 .bind(now)
326 .bind(now)
327 .execute(&self.pool)
328 .await?;
329 tracing::warn!(
330 "Took over stale engine lock from {existing_id} (last heartbeat {:.0}s ago)",
331 now - last_hb
332 );
333 Ok(())
334 } else {
335 let age = now - last_hb;
336 anyhow::bail!(
337 "Another assay engine instance is already running (id: {existing_id}, \
338 last heartbeat {age:.0}s ago).\n\n\
339 SQLite only supports a single engine instance. For multi-instance \
340 deployment (Kubernetes, Docker Swarm), use PostgreSQL:\n\n\
341 \x20 assay serve --backend postgres://user:pass@host:5432/dbname"
342 );
343 }
344 } else {
345 anyhow::bail!("Unexpected engine lock state");
346 }
347 }
348 }
349 }
350
351 pub async fn refresh_engine_lock(&self) -> Result<()> {
353 sqlx::query("UPDATE engine.lock SET last_heartbeat = ? WHERE id = 1 AND instance_id = ?")
354 .bind(timestamp_now())
355 .bind(&self.instance_id)
356 .execute(&self.pool)
357 .await?;
358 Ok(())
359 }
360
361 pub async fn release_engine_lock(&self) -> Result<()> {
363 sqlx::query("DELETE FROM engine.lock WHERE id = 1 AND instance_id = ?")
364 .bind(&self.instance_id)
365 .execute(&self.pool)
366 .await?;
367 Ok(())
368 }
369
370 pub fn spawn_lock_heartbeat(self: &std::sync::Arc<Self>) {
372 let store = std::sync::Arc::clone(self);
373 tokio::spawn(async move {
374 let mut tick = tokio::time::interval(std::time::Duration::from_secs(LOCK_HEARTBEAT_SECS));
375 loop {
376 tick.tick().await;
377 if let Err(e) = store.refresh_engine_lock().await {
378 tracing::error!("Engine lock heartbeat failed: {e}");
379 }
380 }
381 });
382 }
383
384 async fn migrate(&self) -> Result<()> {
397 for statement in SCHEMA.split(';') {
398 let trimmed = statement.trim();
399 if !trimmed.is_empty() {
400 sqlx::query(trimmed).execute(&self.pool).await?;
401 }
402 }
403 Ok(())
405 }
406
407 #[allow(dead_code)]
417 async fn add_column_if_missing(
418 pool: &SqlitePool,
419 table: &str,
420 column: &str,
421 type_def: &str,
422 ) -> Result<()> {
423 let exists: Option<(String,)> =
424 sqlx::query_as("SELECT name FROM pragma_table_info(?) WHERE name = ?")
425 .bind(table)
426 .bind(column)
427 .fetch_optional(pool)
428 .await?;
429 if exists.is_none() {
430 let sql = format!("ALTER TABLE {table} ADD COLUMN {column} {type_def}");
431 sqlx::query(&sql).execute(pool).await?;
432 }
433 Ok(())
434 }
435}
436
437impl WorkflowStore for SqliteStore {
438 async fn create_namespace(&self, name: &str) -> Result<()> {
441 sqlx::query("INSERT INTO workflow.namespaces (name, created_at) VALUES (?, ?)")
442 .bind(name)
443 .bind(timestamp_now())
444 .execute(&self.pool)
445 .await?;
446 Ok(())
447 }
448
449 async fn list_namespaces(&self) -> Result<Vec<NamespaceRecord>> {
450 let rows = sqlx::query_as::<_, (String, f64)>(
451 "SELECT name, created_at FROM workflow.namespaces ORDER BY name",
452 )
453 .fetch_all(&self.pool)
454 .await?;
455 Ok(rows
456 .into_iter()
457 .map(|(name, created_at)| NamespaceRecord { name, created_at })
458 .collect())
459 }
460
461 async fn delete_namespace(&self, name: &str) -> Result<bool> {
462 let res = sqlx::query("DELETE FROM workflow.namespaces WHERE name = ? AND name != 'main'")
464 .bind(name)
465 .execute(&self.pool)
466 .await?;
467 Ok(res.rows_affected() > 0)
468 }
469
470 async fn get_namespace_stats(&self, namespace: &str) -> Result<NamespaceStats> {
471 let total: (i64,) =
472 sqlx::query_as("SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ?")
473 .bind(namespace)
474 .fetch_one(&self.pool)
475 .await?;
476 let running: (i64,) = sqlx::query_as(
477 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'RUNNING'",
478 )
479 .bind(namespace)
480 .fetch_one(&self.pool)
481 .await?;
482 let pending: (i64,) = sqlx::query_as(
483 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'PENDING'",
484 )
485 .bind(namespace)
486 .fetch_one(&self.pool)
487 .await?;
488 let completed: (i64,) = sqlx::query_as(
489 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'COMPLETED'",
490 )
491 .bind(namespace)
492 .fetch_one(&self.pool)
493 .await?;
494 let failed: (i64,) = sqlx::query_as(
495 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'FAILED'",
496 )
497 .bind(namespace)
498 .fetch_one(&self.pool)
499 .await?;
500 let schedules: (i64,) =
501 sqlx::query_as("SELECT COUNT(*) FROM workflow.schedules WHERE namespace = ?")
502 .bind(namespace)
503 .fetch_one(&self.pool)
504 .await?;
505 let workers: (i64,) =
506 sqlx::query_as("SELECT COUNT(*) FROM workflow.workers WHERE namespace = ?")
507 .bind(namespace)
508 .fetch_one(&self.pool)
509 .await?;
510
511 Ok(NamespaceStats {
512 namespace: namespace.to_string(),
513 total_workflows: total.0,
514 running: running.0,
515 pending: pending.0,
516 completed: completed.0,
517 failed: failed.0,
518 schedules: schedules.0,
519 workers: workers.0,
520 })
521 }
522
523 async fn create_workflow(&self, wf: &WorkflowRecord) -> Result<()> {
526 sqlx::query(
527 "INSERT INTO workflow.workflows (id, namespace, run_id, workflow_type, task_queue, status, input, result, error, parent_id, claimed_by, search_attributes, archived_at, archive_uri, created_at, updated_at, completed_at)
528 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
529 )
530 .bind(&wf.id)
531 .bind(&wf.namespace)
532 .bind(&wf.run_id)
533 .bind(&wf.workflow_type)
534 .bind(&wf.task_queue)
535 .bind(&wf.status)
536 .bind(&wf.input)
537 .bind(&wf.result)
538 .bind(&wf.error)
539 .bind(&wf.parent_id)
540 .bind(&wf.claimed_by)
541 .bind(&wf.search_attributes)
542 .bind(wf.archived_at)
543 .bind(&wf.archive_uri)
544 .bind(wf.created_at)
545 .bind(wf.updated_at)
546 .bind(wf.completed_at)
547 .execute(&self.pool)
548 .await?;
549 Ok(())
550 }
551
552 async fn get_workflow(&self, id: &str) -> Result<Option<WorkflowRecord>> {
553 let row = sqlx::query_as::<_, SqliteWorkflowRow>(
554 "SELECT id, namespace, run_id, workflow_type, task_queue, status, input, result, error, parent_id, claimed_by, search_attributes, archived_at, archive_uri, created_at, updated_at, completed_at FROM workflow.workflows WHERE id = ?",
555 )
556 .bind(id)
557 .fetch_optional(&self.pool)
558 .await?;
559 Ok(row.map(Into::into))
560 }
561
562 async fn list_workflows(
563 &self,
564 namespace: &str,
565 status: Option<WorkflowStatus>,
566 workflow_type: Option<&str>,
567 search_attrs_filter: Option<&str>,
568 limit: i64,
569 offset: i64,
570 ) -> Result<Vec<WorkflowRecord>> {
571 let status_str = status.map(|s| s.to_string());
572
573 let filter_pairs: Vec<(String, serde_json::Value)> = search_attrs_filter
578 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
579 .and_then(|v| v.as_object().cloned())
580 .map(|m| m.into_iter().collect())
581 .unwrap_or_default();
582
583 let mut sql = String::from(
584 "SELECT id, namespace, run_id, workflow_type, task_queue, status, input, result, error, parent_id, claimed_by, search_attributes, archived_at, archive_uri, created_at, updated_at, completed_at
585 FROM workflow.workflows
586 WHERE namespace = ?
587 AND (? IS NULL OR status = ?)
588 AND (? IS NULL OR workflow_type = ?)",
589 );
590 for _ in &filter_pairs {
591 sql.push_str(" AND json_extract(search_attributes, '$.' || ?) = ?");
592 }
593 sql.push_str(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
594
595 let mut q = sqlx::query_as::<_, SqliteWorkflowRow>(&sql)
596 .bind(namespace)
597 .bind(&status_str)
598 .bind(&status_str)
599 .bind(workflow_type)
600 .bind(workflow_type);
601 for (key, value) in &filter_pairs {
602 q = q.bind(key.clone());
603 match value {
608 serde_json::Value::String(s) => q = q.bind(s.clone()),
609 serde_json::Value::Number(n) => {
610 if let Some(i) = n.as_i64() {
611 q = q.bind(i);
612 } else if let Some(f) = n.as_f64() {
613 q = q.bind(f);
614 } else {
615 q = q.bind(n.to_string());
616 }
617 }
618 serde_json::Value::Bool(b) => q = q.bind(*b as i64),
619 _ => q = q.bind(value.to_string()),
620 }
621 }
622 let rows = q
623 .bind(limit)
624 .bind(offset)
625 .fetch_all(&self.pool)
626 .await?;
627 Ok(rows.into_iter().map(Into::into).collect())
628 }
629
630 async fn update_workflow_status(
631 &self,
632 id: &str,
633 status: WorkflowStatus,
634 result: Option<&str>,
635 error: Option<&str>,
636 ) -> Result<()> {
637 let now = timestamp_now();
638 let completed_at = if status.is_terminal() { Some(now) } else { None };
639 sqlx::query(
640 "UPDATE workflow.workflows SET status = ?, result = COALESCE(?, result), error = COALESCE(?, error), updated_at = ?, completed_at = COALESCE(?, completed_at) WHERE id = ?",
641 )
642 .bind(status.to_string())
643 .bind(result)
644 .bind(error)
645 .bind(now)
646 .bind(completed_at)
647 .bind(id)
648 .execute(&self.pool)
649 .await?;
650 Ok(())
651 }
652
653 async fn claim_workflow(&self, id: &str, worker_id: &str) -> Result<bool> {
654 let res = sqlx::query(
655 "UPDATE workflow.workflows SET claimed_by = ?, status = 'RUNNING', updated_at = ? WHERE id = ? AND claimed_by IS NULL",
656 )
657 .bind(worker_id)
658 .bind(timestamp_now())
659 .bind(id)
660 .execute(&self.pool)
661 .await?;
662 Ok(res.rows_affected() > 0)
663 }
664
665 async fn mark_workflow_dispatchable(&self, workflow_id: &str) -> Result<()> {
666 sqlx::query("UPDATE workflow.workflows SET needs_dispatch = 1 WHERE id = ?")
667 .bind(workflow_id)
668 .execute(&self.pool)
669 .await?;
670 Ok(())
671 }
672
673 async fn claim_workflow_task(
674 &self,
675 task_queue: &str,
676 worker_id: &str,
677 ) -> Result<Option<WorkflowRecord>> {
678 let now = timestamp_now();
679 let row = sqlx::query_as::<_, SqliteWorkflowRow>(
681 "UPDATE workflow.workflows
682 SET dispatch_claimed_by = ?, dispatch_last_heartbeat = ?, needs_dispatch = 0
683 WHERE id = (
684 SELECT id FROM workflow.workflows
685 WHERE task_queue = ?
686 AND needs_dispatch = 1
687 AND dispatch_claimed_by IS NULL
688 AND status NOT IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMED_OUT')
689 ORDER BY updated_at ASC
690 LIMIT 1
691 )
692 RETURNING id, namespace, run_id, workflow_type, task_queue, status, input, result, error, parent_id, claimed_by, search_attributes, archived_at, archive_uri, created_at, updated_at, completed_at",
693 )
694 .bind(worker_id)
695 .bind(now)
696 .bind(task_queue)
697 .fetch_optional(&self.pool)
698 .await?;
699 Ok(row.map(Into::into))
700 }
701
702 async fn release_workflow_task(&self, workflow_id: &str, worker_id: &str) -> Result<()> {
703 sqlx::query(
704 "UPDATE workflow.workflows
705 SET dispatch_claimed_by = NULL, dispatch_last_heartbeat = NULL
706 WHERE id = ? AND dispatch_claimed_by = ?",
707 )
708 .bind(workflow_id)
709 .bind(worker_id)
710 .execute(&self.pool)
711 .await?;
712 Ok(())
713 }
714
715 async fn release_stale_dispatch_leases(
716 &self,
717 now: f64,
718 timeout_secs: f64,
719 ) -> Result<u64> {
720 let res = sqlx::query(
724 "UPDATE workflow.workflows
725 SET dispatch_claimed_by = NULL,
726 dispatch_last_heartbeat = NULL,
727 needs_dispatch = 1
728 WHERE dispatch_claimed_by IS NOT NULL
729 AND (? - dispatch_last_heartbeat) > ?
730 AND status NOT IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMED_OUT')",
731 )
732 .bind(now)
733 .bind(timeout_secs)
734 .execute(&self.pool)
735 .await?;
736 Ok(res.rows_affected())
737 }
738
739 async fn append_event(&self, ev: &WorkflowEvent) -> Result<i64> {
742 let res = sqlx::query(
743 "INSERT INTO workflow.events (workflow_id, seq, event_type, payload, timestamp) VALUES (?, ?, ?, ?, ?)",
744 )
745 .bind(&ev.workflow_id)
746 .bind(ev.seq)
747 .bind(&ev.event_type)
748 .bind(&ev.payload)
749 .bind(ev.timestamp)
750 .execute(&self.pool)
751 .await?;
752 Ok(res.last_insert_rowid())
753 }
754
755 async fn list_events(&self, workflow_id: &str) -> Result<Vec<WorkflowEvent>> {
756 let rows = sqlx::query_as::<_, SqliteEventRow>(
757 "SELECT id, workflow_id, seq, event_type, payload, timestamp FROM workflow.events WHERE workflow_id = ? ORDER BY seq ASC",
758 )
759 .bind(workflow_id)
760 .fetch_all(&self.pool)
761 .await?;
762 Ok(rows.into_iter().map(Into::into).collect())
763 }
764
765 async fn get_event_count(&self, workflow_id: &str) -> Result<i64> {
766 let row: (i64,) =
767 sqlx::query_as("SELECT COUNT(*) FROM workflow.events WHERE workflow_id = ?")
768 .bind(workflow_id)
769 .fetch_one(&self.pool)
770 .await?;
771 Ok(row.0)
772 }
773
774 async fn create_activity(&self, act: &WorkflowActivity) -> Result<i64> {
777 let res = sqlx::query(
778 "INSERT INTO workflow.activities (workflow_id, seq, name, task_queue, input, status, attempt, max_attempts, initial_interval_secs, backoff_coefficient, start_to_close_secs, heartbeat_timeout_secs, scheduled_at)
779 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
780 )
781 .bind(&act.workflow_id)
782 .bind(act.seq)
783 .bind(&act.name)
784 .bind(&act.task_queue)
785 .bind(&act.input)
786 .bind(&act.status)
787 .bind(act.attempt)
788 .bind(act.max_attempts)
789 .bind(act.initial_interval_secs)
790 .bind(act.backoff_coefficient)
791 .bind(act.start_to_close_secs)
792 .bind(act.heartbeat_timeout_secs)
793 .bind(act.scheduled_at)
794 .execute(&self.pool)
795 .await?;
796 Ok(res.last_insert_rowid())
797 }
798
799 async fn get_activity(&self, id: i64) -> Result<Option<WorkflowActivity>> {
800 let row = sqlx::query_as::<_, SqliteActivityRow>(
801 "SELECT id, workflow_id, seq, name, task_queue, input, status, result, error, attempt, max_attempts, initial_interval_secs, backoff_coefficient, start_to_close_secs, heartbeat_timeout_secs, claimed_by, scheduled_at, started_at, completed_at, last_heartbeat
802 FROM workflow.activities WHERE id = ?",
803 )
804 .bind(id)
805 .fetch_optional(&self.pool)
806 .await?;
807 Ok(row.map(Into::into))
808 }
809
810 async fn get_activity_by_workflow_seq(
811 &self,
812 workflow_id: &str,
813 seq: i32,
814 ) -> Result<Option<WorkflowActivity>> {
815 let row = sqlx::query_as::<_, SqliteActivityRow>(
816 "SELECT id, workflow_id, seq, name, task_queue, input, status, result, error, attempt, max_attempts, initial_interval_secs, backoff_coefficient, start_to_close_secs, heartbeat_timeout_secs, claimed_by, scheduled_at, started_at, completed_at, last_heartbeat
817 FROM workflow.activities WHERE workflow_id = ? AND seq = ?",
818 )
819 .bind(workflow_id)
820 .bind(seq)
821 .fetch_optional(&self.pool)
822 .await?;
823 Ok(row.map(Into::into))
824 }
825
826 async fn claim_activity(
827 &self,
828 task_queue: &str,
829 worker_id: &str,
830 ) -> Result<Option<WorkflowActivity>> {
831 let now = timestamp_now();
832 let row = sqlx::query_as::<_, SqliteActivityRow>(
833 "UPDATE workflow.activities SET status = 'RUNNING', claimed_by = ?, started_at = ?
834 WHERE id = (
835 SELECT id FROM workflow.activities
836 WHERE task_queue = ? AND status = 'PENDING'
837 ORDER BY scheduled_at ASC
838 LIMIT 1
839 )
840 RETURNING id, workflow_id, seq, name, task_queue, input, status, result, error, attempt, max_attempts, initial_interval_secs, backoff_coefficient, start_to_close_secs, heartbeat_timeout_secs, claimed_by, scheduled_at, started_at, completed_at, last_heartbeat",
841 )
842 .bind(worker_id)
843 .bind(now)
844 .bind(task_queue)
845 .fetch_optional(&self.pool)
846 .await?;
847 Ok(row.map(Into::into))
848 }
849
850 async fn requeue_activity_for_retry(
851 &self,
852 id: i64,
853 next_attempt: i32,
854 next_scheduled_at: f64,
855 ) -> Result<()> {
856 sqlx::query(
857 "UPDATE workflow.activities
858 SET status = 'PENDING', attempt = ?, scheduled_at = ?,
859 claimed_by = NULL, started_at = NULL, last_heartbeat = NULL,
860 error = NULL
861 WHERE id = ?",
862 )
863 .bind(next_attempt)
864 .bind(next_scheduled_at)
865 .bind(id)
866 .execute(&self.pool)
867 .await?;
868 Ok(())
869 }
870
871 async fn complete_activity(
872 &self,
873 id: i64,
874 result: Option<&str>,
875 error: Option<&str>,
876 failed: bool,
877 ) -> Result<()> {
878 let status = if failed { "FAILED" } else { "COMPLETED" };
879 sqlx::query(
880 "UPDATE workflow.activities SET status = ?, result = ?, error = ?, completed_at = ? WHERE id = ?",
881 )
882 .bind(status)
883 .bind(result)
884 .bind(error)
885 .bind(timestamp_now())
886 .bind(id)
887 .execute(&self.pool)
888 .await?;
889 Ok(())
890 }
891
892 async fn heartbeat_activity(&self, id: i64, _details: Option<&str>) -> Result<()> {
893 sqlx::query("UPDATE workflow.activities SET last_heartbeat = ? WHERE id = ?")
894 .bind(timestamp_now())
895 .bind(id)
896 .execute(&self.pool)
897 .await?;
898 Ok(())
899 }
900
901 async fn get_timed_out_activities(&self, now: f64) -> Result<Vec<WorkflowActivity>> {
902 let rows = sqlx::query_as::<_, SqliteActivityRow>(
903 "SELECT id, workflow_id, seq, name, task_queue, input, status, result, error, attempt, max_attempts, initial_interval_secs, backoff_coefficient, start_to_close_secs, heartbeat_timeout_secs, claimed_by, scheduled_at, started_at, completed_at, last_heartbeat
904 FROM workflow.activities
905 WHERE status = 'RUNNING'
906 AND heartbeat_timeout_secs IS NOT NULL
907 AND last_heartbeat IS NOT NULL
908 AND (? - last_heartbeat) > heartbeat_timeout_secs",
909 )
910 .bind(now)
911 .fetch_all(&self.pool)
912 .await?;
913 Ok(rows.into_iter().map(Into::into).collect())
914 }
915
916 async fn create_timer(&self, timer: &WorkflowTimer) -> Result<i64> {
919 let res = sqlx::query(
922 "INSERT OR IGNORE INTO workflow.timers (workflow_id, seq, fire_at, fired) VALUES (?, ?, ?, 0)",
923 )
924 .bind(&timer.workflow_id)
925 .bind(timer.seq)
926 .bind(timer.fire_at)
927 .execute(&self.pool)
928 .await?;
929
930 let id = res.last_insert_rowid();
931 if id != 0 {
932 return Ok(id);
933 }
934
935 let (existing_id,): (i64,) = sqlx::query_as(
937 "SELECT id FROM workflow.timers WHERE workflow_id = ? AND seq = ?",
938 )
939 .bind(&timer.workflow_id)
940 .bind(timer.seq)
941 .fetch_one(&self.pool)
942 .await?;
943 Ok(existing_id)
944 }
945
946 async fn cancel_pending_activities(&self, workflow_id: &str) -> Result<u64> {
947 let res = sqlx::query(
948 "UPDATE workflow.activities SET status = 'CANCELLED', completed_at = ?
949 WHERE workflow_id = ? AND status = 'PENDING'",
950 )
951 .bind(timestamp_now())
952 .bind(workflow_id)
953 .execute(&self.pool)
954 .await?;
955 Ok(res.rows_affected())
956 }
957
958 async fn cancel_pending_timers(&self, workflow_id: &str) -> Result<u64> {
959 let res = sqlx::query(
960 "UPDATE workflow.timers SET fired = 1
961 WHERE workflow_id = ? AND fired = 0",
962 )
963 .bind(workflow_id)
964 .execute(&self.pool)
965 .await?;
966 Ok(res.rows_affected())
967 }
968
969 async fn get_timer_by_workflow_seq(
970 &self,
971 workflow_id: &str,
972 seq: i32,
973 ) -> Result<Option<WorkflowTimer>> {
974 let row = sqlx::query_as::<_, SqliteTimerRow>(
975 "SELECT id, workflow_id, seq, fire_at, fired
976 FROM workflow.timers WHERE workflow_id = ? AND seq = ?",
977 )
978 .bind(workflow_id)
979 .bind(seq)
980 .fetch_optional(&self.pool)
981 .await?;
982 Ok(row.map(Into::into))
983 }
984
985 async fn fire_due_timers(&self, now: f64) -> Result<Vec<WorkflowTimer>> {
986 let rows = sqlx::query_as::<_, SqliteTimerRow>(
987 "UPDATE workflow.timers SET fired = 1
988 WHERE fired = 0 AND fire_at <= ?
989 RETURNING id, workflow_id, seq, fire_at, fired",
990 )
991 .bind(now)
992 .fetch_all(&self.pool)
993 .await?;
994 Ok(rows.into_iter().map(Into::into).collect())
995 }
996
997 async fn send_signal(&self, sig: &WorkflowSignal) -> Result<i64> {
1000 let res = sqlx::query(
1001 "INSERT INTO workflow.signals (workflow_id, name, payload, consumed, received_at) VALUES (?, ?, ?, 0, ?)",
1002 )
1003 .bind(&sig.workflow_id)
1004 .bind(&sig.name)
1005 .bind(&sig.payload)
1006 .bind(sig.received_at)
1007 .execute(&self.pool)
1008 .await?;
1009 Ok(res.last_insert_rowid())
1010 }
1011
1012 async fn consume_signals(
1013 &self,
1014 workflow_id: &str,
1015 name: &str,
1016 ) -> Result<Vec<WorkflowSignal>> {
1017 let rows = sqlx::query_as::<_, SqliteSignalRow>(
1018 "UPDATE workflow.signals SET consumed = 1
1019 WHERE workflow_id = ? AND name = ? AND consumed = 0
1020 RETURNING id, workflow_id, name, payload, consumed, received_at",
1021 )
1022 .bind(workflow_id)
1023 .bind(name)
1024 .fetch_all(&self.pool)
1025 .await?;
1026 Ok(rows.into_iter().map(Into::into).collect())
1027 }
1028
1029 async fn create_schedule(&self, sched: &WorkflowSchedule) -> Result<()> {
1032 sqlx::query(
1033 "INSERT INTO workflow.schedules (name, namespace, workflow_type, cron_expr, timezone, input, task_queue, overlap_policy, paused, last_run_at, next_run_at, last_workflow_id, created_at)
1034 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1035 )
1036 .bind(&sched.name)
1037 .bind(&sched.namespace)
1038 .bind(&sched.workflow_type)
1039 .bind(&sched.cron_expr)
1040 .bind(&sched.timezone)
1041 .bind(&sched.input)
1042 .bind(&sched.task_queue)
1043 .bind(&sched.overlap_policy)
1044 .bind(sched.paused)
1045 .bind(sched.last_run_at)
1046 .bind(sched.next_run_at)
1047 .bind(&sched.last_workflow_id)
1048 .bind(sched.created_at)
1049 .execute(&self.pool)
1050 .await?;
1051 Ok(())
1052 }
1053
1054 async fn get_schedule(&self, namespace: &str, name: &str) -> Result<Option<WorkflowSchedule>> {
1055 let row = sqlx::query_as::<_, SqliteScheduleRow>(
1056 "SELECT name, namespace, workflow_type, cron_expr, timezone, input, task_queue, overlap_policy, paused, last_run_at, next_run_at, last_workflow_id, created_at
1057 FROM workflow.schedules WHERE namespace = ? AND name = ?",
1058 )
1059 .bind(namespace)
1060 .bind(name)
1061 .fetch_optional(&self.pool)
1062 .await?;
1063 Ok(row.map(Into::into))
1064 }
1065
1066 async fn list_schedules(&self, namespace: &str) -> Result<Vec<WorkflowSchedule>> {
1067 let rows = sqlx::query_as::<_, SqliteScheduleRow>(
1068 "SELECT name, namespace, workflow_type, cron_expr, timezone, input, task_queue, overlap_policy, paused, last_run_at, next_run_at, last_workflow_id, created_at
1069 FROM workflow.schedules WHERE namespace = ? ORDER BY name",
1070 )
1071 .bind(namespace)
1072 .fetch_all(&self.pool)
1073 .await?;
1074 Ok(rows.into_iter().map(Into::into).collect())
1075 }
1076
1077 async fn update_schedule_last_run(
1078 &self,
1079 namespace: &str,
1080 name: &str,
1081 last_run_at: f64,
1082 next_run_at: f64,
1083 workflow_id: &str,
1084 ) -> Result<()> {
1085 sqlx::query(
1086 "UPDATE workflow.schedules SET last_run_at = ?, next_run_at = ?, last_workflow_id = ? WHERE namespace = ? AND name = ?",
1087 )
1088 .bind(last_run_at)
1089 .bind(next_run_at)
1090 .bind(workflow_id)
1091 .bind(namespace)
1092 .bind(name)
1093 .execute(&self.pool)
1094 .await?;
1095 Ok(())
1096 }
1097
1098 async fn delete_schedule(&self, namespace: &str, name: &str) -> Result<bool> {
1099 let res =
1100 sqlx::query("DELETE FROM workflow.schedules WHERE namespace = ? AND name = ?")
1101 .bind(namespace)
1102 .bind(name)
1103 .execute(&self.pool)
1104 .await?;
1105 Ok(res.rows_affected() > 0)
1106 }
1107
1108 async fn list_archivable_workflows(
1109 &self,
1110 cutoff: f64,
1111 limit: i64,
1112 ) -> Result<Vec<WorkflowRecord>> {
1113 let rows = sqlx::query_as::<_, SqliteWorkflowRow>(
1114 "SELECT id, namespace, run_id, workflow_type, task_queue, status, input, result, error, parent_id, claimed_by, search_attributes, archived_at, archive_uri, created_at, updated_at, completed_at
1115 FROM workflow.workflows
1116 WHERE status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMED_OUT')
1117 AND completed_at IS NOT NULL
1118 AND completed_at < ?
1119 AND archived_at IS NULL
1120 ORDER BY completed_at ASC
1121 LIMIT ?",
1122 )
1123 .bind(cutoff)
1124 .bind(limit)
1125 .fetch_all(&self.pool)
1126 .await?;
1127 Ok(rows.into_iter().map(Into::into).collect())
1128 }
1129
1130 async fn mark_archived_and_purge(
1131 &self,
1132 workflow_id: &str,
1133 archive_uri: &str,
1134 archived_at: f64,
1135 ) -> Result<()> {
1136 let mut tx = self.pool.begin().await?;
1137 sqlx::query("DELETE FROM workflow.events WHERE workflow_id = ?")
1138 .bind(workflow_id)
1139 .execute(&mut *tx)
1140 .await?;
1141 sqlx::query("DELETE FROM workflow.activities WHERE workflow_id = ?")
1142 .bind(workflow_id)
1143 .execute(&mut *tx)
1144 .await?;
1145 sqlx::query("DELETE FROM workflow.timers WHERE workflow_id = ?")
1146 .bind(workflow_id)
1147 .execute(&mut *tx)
1148 .await?;
1149 sqlx::query("DELETE FROM workflow.signals WHERE workflow_id = ?")
1150 .bind(workflow_id)
1151 .execute(&mut *tx)
1152 .await?;
1153 sqlx::query("DELETE FROM workflow.snapshots WHERE workflow_id = ?")
1154 .bind(workflow_id)
1155 .execute(&mut *tx)
1156 .await?;
1157 sqlx::query(
1158 "UPDATE workflow.workflows SET archived_at = ?, archive_uri = ? WHERE id = ?",
1159 )
1160 .bind(archived_at)
1161 .bind(archive_uri)
1162 .bind(workflow_id)
1163 .execute(&mut *tx)
1164 .await?;
1165 tx.commit().await?;
1166 Ok(())
1167 }
1168
1169 async fn upsert_search_attributes(
1170 &self,
1171 workflow_id: &str,
1172 patch_json: &str,
1173 ) -> Result<()> {
1174 let current: Option<(Option<String>,)> =
1177 sqlx::query_as("SELECT search_attributes FROM workflow.workflows WHERE id = ?")
1178 .bind(workflow_id)
1179 .fetch_optional(&self.pool)
1180 .await?;
1181 let merged = merge_search_attrs(
1182 current.and_then(|(s,)| s).as_deref(),
1183 patch_json,
1184 )?;
1185 sqlx::query("UPDATE workflow.workflows SET search_attributes = ? WHERE id = ?")
1186 .bind(merged)
1187 .bind(workflow_id)
1188 .execute(&self.pool)
1189 .await?;
1190 Ok(())
1191 }
1192
1193 async fn update_schedule(
1194 &self,
1195 namespace: &str,
1196 name: &str,
1197 patch: &SchedulePatch,
1198 ) -> Result<Option<WorkflowSchedule>> {
1199 let mut sets: Vec<&'static str> = Vec::new();
1202 if patch.cron_expr.is_some() {
1203 sets.push("cron_expr = ?");
1204 }
1205 if patch.timezone.is_some() {
1206 sets.push("timezone = ?");
1207 }
1208 if patch.input.is_some() {
1209 sets.push("input = ?");
1210 }
1211 if patch.task_queue.is_some() {
1212 sets.push("task_queue = ?");
1213 }
1214 if patch.overlap_policy.is_some() {
1215 sets.push("overlap_policy = ?");
1216 }
1217 if sets.is_empty() {
1219 return self.get_schedule(namespace, name).await;
1220 }
1221
1222 let sql = format!(
1223 "UPDATE workflow.schedules SET {} WHERE namespace = ? AND name = ?",
1224 sets.join(", ")
1225 );
1226 let mut q = sqlx::query(&sql);
1227 if let Some(ref v) = patch.cron_expr {
1228 q = q.bind(v);
1229 }
1230 if let Some(ref v) = patch.timezone {
1231 q = q.bind(v);
1232 }
1233 if let Some(ref v) = patch.input {
1234 q = q.bind(v.to_string());
1235 }
1236 if let Some(ref v) = patch.task_queue {
1237 q = q.bind(v);
1238 }
1239 if let Some(ref v) = patch.overlap_policy {
1240 q = q.bind(v);
1241 }
1242 let res = q
1243 .bind(namespace)
1244 .bind(name)
1245 .execute(&self.pool)
1246 .await?;
1247 if res.rows_affected() == 0 {
1248 return Ok(None);
1249 }
1250 self.get_schedule(namespace, name).await
1251 }
1252
1253 async fn set_schedule_paused(
1254 &self,
1255 namespace: &str,
1256 name: &str,
1257 paused: bool,
1258 ) -> Result<Option<WorkflowSchedule>> {
1259 let res = sqlx::query(
1260 "UPDATE workflow.schedules SET paused = ? WHERE namespace = ? AND name = ?",
1261 )
1262 .bind(paused)
1263 .bind(namespace)
1264 .bind(name)
1265 .execute(&self.pool)
1266 .await?;
1267 if res.rows_affected() == 0 {
1268 return Ok(None);
1269 }
1270 self.get_schedule(namespace, name).await
1271 }
1272
1273 async fn register_worker(&self, w: &WorkflowWorker) -> Result<()> {
1276 sqlx::query(
1277 "INSERT OR REPLACE INTO workflow.workers (id, namespace, identity, task_queue, workflows, activities, max_concurrent_workflows, max_concurrent_activities, active_tasks, last_heartbeat, registered_at)
1278 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1279 )
1280 .bind(&w.id)
1281 .bind(&w.namespace)
1282 .bind(&w.identity)
1283 .bind(&w.task_queue)
1284 .bind(&w.workflows)
1285 .bind(&w.activities)
1286 .bind(w.max_concurrent_workflows)
1287 .bind(w.max_concurrent_activities)
1288 .bind(w.active_tasks)
1289 .bind(w.last_heartbeat)
1290 .bind(w.registered_at)
1291 .execute(&self.pool)
1292 .await?;
1293 Ok(())
1294 }
1295
1296 async fn heartbeat_worker(&self, id: &str, now: f64) -> Result<()> {
1297 sqlx::query("UPDATE workflow.workers SET last_heartbeat = ? WHERE id = ?")
1298 .bind(now)
1299 .bind(id)
1300 .execute(&self.pool)
1301 .await?;
1302 Ok(())
1303 }
1304
1305 async fn list_workers(&self, namespace: &str) -> Result<Vec<WorkflowWorker>> {
1306 let rows = sqlx::query_as::<_, SqliteWorkerRow>(
1307 "SELECT id, namespace, identity, task_queue, workflows, activities, max_concurrent_workflows, max_concurrent_activities, active_tasks, last_heartbeat, registered_at
1308 FROM workflow.workers WHERE namespace = ? ORDER BY registered_at",
1309 )
1310 .bind(namespace)
1311 .fetch_all(&self.pool)
1312 .await?;
1313 Ok(rows.into_iter().map(Into::into).collect())
1314 }
1315
1316 async fn remove_dead_workers(&self, cutoff: f64) -> Result<Vec<String>> {
1317 let rows: Vec<(String,)> =
1318 sqlx::query_as("SELECT id FROM workflow.workers WHERE last_heartbeat < ?")
1319 .bind(cutoff)
1320 .fetch_all(&self.pool)
1321 .await?;
1322 let ids: Vec<String> = rows.into_iter().map(|r| r.0).collect();
1323 if !ids.is_empty() {
1324 sqlx::query("DELETE FROM workflow.workers WHERE last_heartbeat < ?")
1325 .bind(cutoff)
1326 .execute(&self.pool)
1327 .await?;
1328 }
1329 Ok(ids)
1330 }
1331
1332 async fn list_child_workflows(&self, parent_id: &str) -> Result<Vec<WorkflowRecord>> {
1335 let rows = sqlx::query_as::<_, SqliteWorkflowRow>(
1336 "SELECT id, namespace, run_id, workflow_type, task_queue, status, input, result, error, parent_id, claimed_by, search_attributes, archived_at, archive_uri, created_at, updated_at, completed_at
1337 FROM workflow.workflows WHERE parent_id = ? ORDER BY created_at ASC",
1338 )
1339 .bind(parent_id)
1340 .fetch_all(&self.pool)
1341 .await?;
1342 Ok(rows.into_iter().map(Into::into).collect())
1343 }
1344
1345 async fn create_snapshot(
1348 &self,
1349 workflow_id: &str,
1350 event_seq: i32,
1351 state_json: &str,
1352 ) -> Result<()> {
1353 sqlx::query(
1354 "INSERT OR REPLACE INTO workflow.snapshots (workflow_id, event_seq, state_json, created_at)
1355 VALUES (?, ?, ?, ?)",
1356 )
1357 .bind(workflow_id)
1358 .bind(event_seq)
1359 .bind(state_json)
1360 .bind(timestamp_now())
1361 .execute(&self.pool)
1362 .await?;
1363 Ok(())
1364 }
1365
1366 async fn get_latest_snapshot(
1367 &self,
1368 workflow_id: &str,
1369 ) -> Result<Option<WorkflowSnapshot>> {
1370 let row = sqlx::query_as::<_, (String, i32, String, f64)>(
1371 "SELECT workflow_id, event_seq, state_json, created_at
1372 FROM workflow.snapshots WHERE workflow_id = ?
1373 ORDER BY event_seq DESC LIMIT 1",
1374 )
1375 .bind(workflow_id)
1376 .fetch_optional(&self.pool)
1377 .await?;
1378
1379 Ok(row.map(|(workflow_id, event_seq, state_json, created_at)| WorkflowSnapshot {
1380 workflow_id,
1381 event_seq,
1382 state_json,
1383 created_at,
1384 }))
1385 }
1386
1387 async fn get_queue_stats(&self, namespace: &str) -> Result<Vec<QueueStats>> {
1390 let rows = sqlx::query_as::<_, (String, i64, i64)>(
1392 "SELECT a.task_queue,
1393 SUM(CASE WHEN a.status = 'PENDING' THEN 1 ELSE 0 END),
1394 SUM(CASE WHEN a.status = 'RUNNING' THEN 1 ELSE 0 END)
1395 FROM workflow.activities a
1396 INNER JOIN workflow.workflows w ON w.id = a.workflow_id
1397 WHERE w.namespace = ?
1398 GROUP BY a.task_queue",
1399 )
1400 .bind(namespace)
1401 .fetch_all(&self.pool)
1402 .await?;
1403
1404 let mut stats: Vec<QueueStats> = rows
1405 .into_iter()
1406 .map(|(queue, pending, running)| QueueStats {
1407 queue,
1408 pending_activities: pending,
1409 running_activities: running,
1410 workers: 0,
1411 })
1412 .collect();
1413
1414 let worker_rows = sqlx::query_as::<_, (String, i64)>(
1416 "SELECT task_queue, COUNT(*) FROM workflow.workers WHERE namespace = ? GROUP BY task_queue",
1417 )
1418 .bind(namespace)
1419 .fetch_all(&self.pool)
1420 .await?;
1421
1422 for (queue, count) in worker_rows {
1423 if let Some(s) = stats.iter_mut().find(|s| s.queue == queue) {
1424 s.workers = count;
1425 } else {
1426 stats.push(QueueStats {
1427 queue,
1428 pending_activities: 0,
1429 running_activities: 0,
1430 workers: count,
1431 });
1432 }
1433 }
1434
1435 stats.sort_by(|a, b| a.queue.cmp(&b.queue));
1436 Ok(stats)
1437 }
1438
1439 async fn try_acquire_scheduler_lock(&self) -> Result<bool> {
1442 self.refresh_engine_lock().await.ok();
1445 Ok(true)
1446 }
1447
1448}
1449
1450fn timestamp_now() -> f64 {
1451 std::time::SystemTime::now()
1452 .duration_since(std::time::UNIX_EPOCH)
1453 .unwrap()
1454 .as_secs_f64()
1455}
1456
1457pub(crate) fn merge_search_attrs(current: Option<&str>, patch_json: &str) -> Result<String> {
1460 let mut current_map: serde_json::Map<String, serde_json::Value> = current
1461 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1462 .and_then(|v| v.as_object().cloned())
1463 .unwrap_or_default();
1464 let patch: serde_json::Value = serde_json::from_str(patch_json)
1465 .map_err(|e| anyhow::anyhow!("invalid search_attributes patch: {e}"))?;
1466 let patch_obj = patch
1467 .as_object()
1468 .ok_or_else(|| anyhow::anyhow!("search_attributes patch must be a JSON object"))?;
1469 for (k, v) in patch_obj {
1470 current_map.insert(k.clone(), v.clone());
1471 }
1472 Ok(serde_json::Value::Object(current_map).to_string())
1473}
1474
1475#[derive(sqlx::FromRow)]
1478struct SqliteWorkflowRow {
1479 id: String,
1480 namespace: String,
1481 run_id: String,
1482 workflow_type: String,
1483 task_queue: String,
1484 status: String,
1485 input: Option<String>,
1486 result: Option<String>,
1487 error: Option<String>,
1488 parent_id: Option<String>,
1489 claimed_by: Option<String>,
1490 search_attributes: Option<String>,
1491 archived_at: Option<f64>,
1492 archive_uri: Option<String>,
1493 created_at: f64,
1494 updated_at: f64,
1495 completed_at: Option<f64>,
1496}
1497
1498impl From<SqliteWorkflowRow> for WorkflowRecord {
1499 fn from(r: SqliteWorkflowRow) -> Self {
1500 Self {
1501 id: r.id,
1502 namespace: r.namespace,
1503 run_id: r.run_id,
1504 workflow_type: r.workflow_type,
1505 task_queue: r.task_queue,
1506 status: r.status,
1507 input: r.input,
1508 result: r.result,
1509 error: r.error,
1510 parent_id: r.parent_id,
1511 claimed_by: r.claimed_by,
1512 search_attributes: r.search_attributes,
1513 archived_at: r.archived_at,
1514 archive_uri: r.archive_uri,
1515 created_at: r.created_at,
1516 updated_at: r.updated_at,
1517 completed_at: r.completed_at,
1518 }
1519 }
1520}
1521
1522#[derive(sqlx::FromRow)]
1523struct SqliteEventRow {
1524 id: i64,
1525 workflow_id: String,
1526 seq: i32,
1527 event_type: String,
1528 payload: Option<String>,
1529 timestamp: f64,
1530}
1531
1532impl From<SqliteEventRow> for WorkflowEvent {
1533 fn from(r: SqliteEventRow) -> Self {
1534 Self {
1535 id: Some(r.id),
1536 workflow_id: r.workflow_id,
1537 seq: r.seq,
1538 event_type: r.event_type,
1539 payload: r.payload,
1540 timestamp: r.timestamp,
1541 }
1542 }
1543}
1544
1545#[derive(sqlx::FromRow)]
1546struct SqliteActivityRow {
1547 id: i64,
1548 workflow_id: String,
1549 seq: i32,
1550 name: String,
1551 task_queue: String,
1552 input: Option<String>,
1553 status: String,
1554 result: Option<String>,
1555 error: Option<String>,
1556 attempt: i32,
1557 max_attempts: i32,
1558 initial_interval_secs: f64,
1559 backoff_coefficient: f64,
1560 start_to_close_secs: f64,
1561 heartbeat_timeout_secs: Option<f64>,
1562 claimed_by: Option<String>,
1563 scheduled_at: f64,
1564 started_at: Option<f64>,
1565 completed_at: Option<f64>,
1566 last_heartbeat: Option<f64>,
1567}
1568
1569impl From<SqliteActivityRow> for WorkflowActivity {
1570 fn from(r: SqliteActivityRow) -> Self {
1571 Self {
1572 id: Some(r.id),
1573 workflow_id: r.workflow_id,
1574 seq: r.seq,
1575 name: r.name,
1576 task_queue: r.task_queue,
1577 input: r.input,
1578 status: r.status,
1579 result: r.result,
1580 error: r.error,
1581 attempt: r.attempt,
1582 max_attempts: r.max_attempts,
1583 initial_interval_secs: r.initial_interval_secs,
1584 backoff_coefficient: r.backoff_coefficient,
1585 start_to_close_secs: r.start_to_close_secs,
1586 heartbeat_timeout_secs: r.heartbeat_timeout_secs,
1587 claimed_by: r.claimed_by,
1588 scheduled_at: r.scheduled_at,
1589 started_at: r.started_at,
1590 completed_at: r.completed_at,
1591 last_heartbeat: r.last_heartbeat,
1592 }
1593 }
1594}
1595
1596#[derive(sqlx::FromRow)]
1597struct SqliteTimerRow {
1598 id: i64,
1599 workflow_id: String,
1600 seq: i32,
1601 fire_at: f64,
1602 fired: bool,
1603}
1604
1605impl From<SqliteTimerRow> for WorkflowTimer {
1606 fn from(r: SqliteTimerRow) -> Self {
1607 Self {
1608 id: Some(r.id),
1609 workflow_id: r.workflow_id,
1610 seq: r.seq,
1611 fire_at: r.fire_at,
1612 fired: r.fired,
1613 }
1614 }
1615}
1616
1617#[derive(sqlx::FromRow)]
1618struct SqliteSignalRow {
1619 id: i64,
1620 workflow_id: String,
1621 name: String,
1622 payload: Option<String>,
1623 consumed: bool,
1624 received_at: f64,
1625}
1626
1627impl From<SqliteSignalRow> for WorkflowSignal {
1628 fn from(r: SqliteSignalRow) -> Self {
1629 Self {
1630 id: Some(r.id),
1631 workflow_id: r.workflow_id,
1632 name: r.name,
1633 payload: r.payload,
1634 consumed: r.consumed,
1635 received_at: r.received_at,
1636 }
1637 }
1638}
1639
1640#[derive(sqlx::FromRow)]
1641struct SqliteScheduleRow {
1642 name: String,
1643 namespace: String,
1644 workflow_type: String,
1645 cron_expr: String,
1646 timezone: String,
1647 input: Option<String>,
1648 task_queue: String,
1649 overlap_policy: String,
1650 paused: bool,
1651 last_run_at: Option<f64>,
1652 next_run_at: Option<f64>,
1653 last_workflow_id: Option<String>,
1654 created_at: f64,
1655}
1656
1657impl From<SqliteScheduleRow> for WorkflowSchedule {
1658 fn from(r: SqliteScheduleRow) -> Self {
1659 Self {
1660 name: r.name,
1661 namespace: r.namespace,
1662 workflow_type: r.workflow_type,
1663 cron_expr: r.cron_expr,
1664 timezone: r.timezone,
1665 input: r.input,
1666 task_queue: r.task_queue,
1667 overlap_policy: r.overlap_policy,
1668 paused: r.paused,
1669 last_run_at: r.last_run_at,
1670 next_run_at: r.next_run_at,
1671 last_workflow_id: r.last_workflow_id,
1672 created_at: r.created_at,
1673 }
1674 }
1675}
1676
1677#[derive(sqlx::FromRow)]
1678struct SqliteWorkerRow {
1679 id: String,
1680 namespace: String,
1681 identity: String,
1682 task_queue: String,
1683 workflows: Option<String>,
1684 activities: Option<String>,
1685 max_concurrent_workflows: i32,
1686 max_concurrent_activities: i32,
1687 active_tasks: i32,
1688 last_heartbeat: f64,
1689 registered_at: f64,
1690}
1691
1692impl From<SqliteWorkerRow> for WorkflowWorker {
1693 fn from(r: SqliteWorkerRow) -> Self {
1694 Self {
1695 id: r.id,
1696 namespace: r.namespace,
1697 identity: r.identity,
1698 task_queue: r.task_queue,
1699 workflows: r.workflows,
1700 activities: r.activities,
1701 max_concurrent_workflows: r.max_concurrent_workflows,
1702 max_concurrent_activities: r.max_concurrent_activities,
1703 active_tasks: r.active_tasks,
1704 last_heartbeat: r.last_heartbeat,
1705 registered_at: r.registered_at,
1706 }
1707 }
1708}