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<()> {
390 for statement in SCHEMA.split(';') {
391 let trimmed = statement.trim();
392 if !trimmed.is_empty() {
393 sqlx::query(trimmed).execute(&self.pool).await?;
394 }
395 }
396 Ok(())
398 }
399
400 #[allow(dead_code)]
410 async fn add_column_if_missing(
411 pool: &SqlitePool,
412 table: &str,
413 column: &str,
414 type_def: &str,
415 ) -> Result<()> {
416 let exists: Option<(String,)> =
417 sqlx::query_as("SELECT name FROM pragma_table_info(?) WHERE name = ?")
418 .bind(table)
419 .bind(column)
420 .fetch_optional(pool)
421 .await?;
422 if exists.is_none() {
423 let sql = format!("ALTER TABLE {table} ADD COLUMN {column} {type_def}");
424 sqlx::query(&sql).execute(pool).await?;
425 }
426 Ok(())
427 }
428}
429
430impl WorkflowStore for SqliteStore {
431 async fn create_namespace(&self, name: &str) -> Result<()> {
434 sqlx::query("INSERT INTO workflow.namespaces (name, created_at) VALUES (?, ?)")
435 .bind(name)
436 .bind(timestamp_now())
437 .execute(&self.pool)
438 .await?;
439 Ok(())
440 }
441
442 async fn list_namespaces(&self) -> Result<Vec<NamespaceRecord>> {
443 let rows = sqlx::query_as::<_, (String, f64)>(
444 "SELECT name, created_at FROM workflow.namespaces ORDER BY name",
445 )
446 .fetch_all(&self.pool)
447 .await?;
448 Ok(rows
449 .into_iter()
450 .map(|(name, created_at)| NamespaceRecord { name, created_at })
451 .collect())
452 }
453
454 async fn delete_namespace(&self, name: &str) -> Result<bool> {
455 let res = sqlx::query("DELETE FROM workflow.namespaces WHERE name = ? AND name != 'main'")
457 .bind(name)
458 .execute(&self.pool)
459 .await?;
460 Ok(res.rows_affected() > 0)
461 }
462
463 async fn get_namespace_stats(&self, namespace: &str) -> Result<NamespaceStats> {
464 let total: (i64,) =
465 sqlx::query_as("SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ?")
466 .bind(namespace)
467 .fetch_one(&self.pool)
468 .await?;
469 let running: (i64,) = sqlx::query_as(
470 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'RUNNING'",
471 )
472 .bind(namespace)
473 .fetch_one(&self.pool)
474 .await?;
475 let pending: (i64,) = sqlx::query_as(
476 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'PENDING'",
477 )
478 .bind(namespace)
479 .fetch_one(&self.pool)
480 .await?;
481 let completed: (i64,) = sqlx::query_as(
482 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'COMPLETED'",
483 )
484 .bind(namespace)
485 .fetch_one(&self.pool)
486 .await?;
487 let failed: (i64,) = sqlx::query_as(
488 "SELECT COUNT(*) FROM workflow.workflows WHERE namespace = ? AND status = 'FAILED'",
489 )
490 .bind(namespace)
491 .fetch_one(&self.pool)
492 .await?;
493 let schedules: (i64,) =
494 sqlx::query_as("SELECT COUNT(*) FROM workflow.schedules WHERE namespace = ?")
495 .bind(namespace)
496 .fetch_one(&self.pool)
497 .await?;
498 let workers: (i64,) =
499 sqlx::query_as("SELECT COUNT(*) FROM workflow.workers WHERE namespace = ?")
500 .bind(namespace)
501 .fetch_one(&self.pool)
502 .await?;
503
504 Ok(NamespaceStats {
505 namespace: namespace.to_string(),
506 total_workflows: total.0,
507 running: running.0,
508 pending: pending.0,
509 completed: completed.0,
510 failed: failed.0,
511 schedules: schedules.0,
512 workers: workers.0,
513 })
514 }
515
516 async fn create_workflow(&self, wf: &WorkflowRecord) -> Result<()> {
519 sqlx::query(
520 "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)
521 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
522 )
523 .bind(&wf.id)
524 .bind(&wf.namespace)
525 .bind(&wf.run_id)
526 .bind(&wf.workflow_type)
527 .bind(&wf.task_queue)
528 .bind(&wf.status)
529 .bind(&wf.input)
530 .bind(&wf.result)
531 .bind(&wf.error)
532 .bind(&wf.parent_id)
533 .bind(&wf.claimed_by)
534 .bind(&wf.search_attributes)
535 .bind(wf.archived_at)
536 .bind(&wf.archive_uri)
537 .bind(wf.created_at)
538 .bind(wf.updated_at)
539 .bind(wf.completed_at)
540 .execute(&self.pool)
541 .await?;
542 Ok(())
543 }
544
545 async fn get_workflow(&self, id: &str) -> Result<Option<WorkflowRecord>> {
546 let row = sqlx::query_as::<_, SqliteWorkflowRow>(
547 "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 = ?",
548 )
549 .bind(id)
550 .fetch_optional(&self.pool)
551 .await?;
552 Ok(row.map(Into::into))
553 }
554
555 async fn list_workflows(
556 &self,
557 namespace: &str,
558 status: Option<WorkflowStatus>,
559 workflow_type: Option<&str>,
560 search_attrs_filter: Option<&str>,
561 limit: i64,
562 offset: i64,
563 ) -> Result<Vec<WorkflowRecord>> {
564 let status_str = status.map(|s| s.to_string());
565
566 let filter_pairs: Vec<(String, serde_json::Value)> = search_attrs_filter
571 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
572 .and_then(|v| v.as_object().cloned())
573 .map(|m| m.into_iter().collect())
574 .unwrap_or_default();
575
576 let mut sql = String::from(
577 "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
578 FROM workflow.workflows
579 WHERE namespace = ?
580 AND (? IS NULL OR status = ?)
581 AND (? IS NULL OR workflow_type = ?)",
582 );
583 for _ in &filter_pairs {
584 sql.push_str(" AND json_extract(search_attributes, '$.' || ?) = ?");
585 }
586 sql.push_str(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
587
588 let mut q = sqlx::query_as::<_, SqliteWorkflowRow>(&sql)
589 .bind(namespace)
590 .bind(&status_str)
591 .bind(&status_str)
592 .bind(workflow_type)
593 .bind(workflow_type);
594 for (key, value) in &filter_pairs {
595 q = q.bind(key.clone());
596 match value {
601 serde_json::Value::String(s) => q = q.bind(s.clone()),
602 serde_json::Value::Number(n) => {
603 if let Some(i) = n.as_i64() {
604 q = q.bind(i);
605 } else if let Some(f) = n.as_f64() {
606 q = q.bind(f);
607 } else {
608 q = q.bind(n.to_string());
609 }
610 }
611 serde_json::Value::Bool(b) => q = q.bind(*b as i64),
612 _ => q = q.bind(value.to_string()),
613 }
614 }
615 let rows = q
616 .bind(limit)
617 .bind(offset)
618 .fetch_all(&self.pool)
619 .await?;
620 Ok(rows.into_iter().map(Into::into).collect())
621 }
622
623 async fn update_workflow_status(
624 &self,
625 id: &str,
626 status: WorkflowStatus,
627 result: Option<&str>,
628 error: Option<&str>,
629 ) -> Result<()> {
630 let now = timestamp_now();
631 let completed_at = if status.is_terminal() { Some(now) } else { None };
632 sqlx::query(
633 "UPDATE workflow.workflows SET status = ?, result = COALESCE(?, result), error = COALESCE(?, error), updated_at = ?, completed_at = COALESCE(?, completed_at) WHERE id = ?",
634 )
635 .bind(status.to_string())
636 .bind(result)
637 .bind(error)
638 .bind(now)
639 .bind(completed_at)
640 .bind(id)
641 .execute(&self.pool)
642 .await?;
643 Ok(())
644 }
645
646 async fn claim_workflow(&self, id: &str, worker_id: &str) -> Result<bool> {
647 let res = sqlx::query(
648 "UPDATE workflow.workflows SET claimed_by = ?, status = 'RUNNING', updated_at = ? WHERE id = ? AND claimed_by IS NULL",
649 )
650 .bind(worker_id)
651 .bind(timestamp_now())
652 .bind(id)
653 .execute(&self.pool)
654 .await?;
655 Ok(res.rows_affected() > 0)
656 }
657
658 async fn mark_workflow_dispatchable(&self, workflow_id: &str) -> Result<()> {
659 sqlx::query("UPDATE workflow.workflows SET needs_dispatch = 1 WHERE id = ?")
660 .bind(workflow_id)
661 .execute(&self.pool)
662 .await?;
663 Ok(())
664 }
665
666 async fn claim_workflow_task(
667 &self,
668 task_queue: &str,
669 worker_id: &str,
670 ) -> Result<Option<WorkflowRecord>> {
671 let now = timestamp_now();
672 let row = sqlx::query_as::<_, SqliteWorkflowRow>(
674 "UPDATE workflow.workflows
675 SET dispatch_claimed_by = ?, dispatch_last_heartbeat = ?, needs_dispatch = 0
676 WHERE id = (
677 SELECT id FROM workflow.workflows
678 WHERE task_queue = ?
679 AND needs_dispatch = 1
680 AND dispatch_claimed_by IS NULL
681 AND status NOT IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMED_OUT')
682 ORDER BY updated_at ASC
683 LIMIT 1
684 )
685 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",
686 )
687 .bind(worker_id)
688 .bind(now)
689 .bind(task_queue)
690 .fetch_optional(&self.pool)
691 .await?;
692 Ok(row.map(Into::into))
693 }
694
695 async fn release_workflow_task(&self, workflow_id: &str, worker_id: &str) -> Result<()> {
696 sqlx::query(
697 "UPDATE workflow.workflows
698 SET dispatch_claimed_by = NULL, dispatch_last_heartbeat = NULL
699 WHERE id = ? AND dispatch_claimed_by = ?",
700 )
701 .bind(workflow_id)
702 .bind(worker_id)
703 .execute(&self.pool)
704 .await?;
705 Ok(())
706 }
707
708 async fn release_stale_dispatch_leases(
709 &self,
710 now: f64,
711 timeout_secs: f64,
712 ) -> Result<u64> {
713 let res = sqlx::query(
717 "UPDATE workflow.workflows
718 SET dispatch_claimed_by = NULL,
719 dispatch_last_heartbeat = NULL,
720 needs_dispatch = 1
721 WHERE dispatch_claimed_by IS NOT NULL
722 AND (? - dispatch_last_heartbeat) > ?
723 AND status NOT IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMED_OUT')",
724 )
725 .bind(now)
726 .bind(timeout_secs)
727 .execute(&self.pool)
728 .await?;
729 Ok(res.rows_affected())
730 }
731
732 async fn append_event(&self, ev: &WorkflowEvent) -> Result<i64> {
735 let res = sqlx::query(
736 "INSERT INTO workflow.events (workflow_id, seq, event_type, payload, timestamp) VALUES (?, ?, ?, ?, ?)",
737 )
738 .bind(&ev.workflow_id)
739 .bind(ev.seq)
740 .bind(&ev.event_type)
741 .bind(&ev.payload)
742 .bind(ev.timestamp)
743 .execute(&self.pool)
744 .await?;
745 Ok(res.last_insert_rowid())
746 }
747
748 async fn list_events(&self, workflow_id: &str) -> Result<Vec<WorkflowEvent>> {
749 let rows = sqlx::query_as::<_, SqliteEventRow>(
750 "SELECT id, workflow_id, seq, event_type, payload, timestamp FROM workflow.events WHERE workflow_id = ? ORDER BY seq ASC",
751 )
752 .bind(workflow_id)
753 .fetch_all(&self.pool)
754 .await?;
755 Ok(rows.into_iter().map(Into::into).collect())
756 }
757
758 async fn get_event_count(&self, workflow_id: &str) -> Result<i64> {
759 let row: (i64,) =
760 sqlx::query_as("SELECT COUNT(*) FROM workflow.events WHERE workflow_id = ?")
761 .bind(workflow_id)
762 .fetch_one(&self.pool)
763 .await?;
764 Ok(row.0)
765 }
766
767 async fn create_activity(&self, act: &WorkflowActivity) -> Result<i64> {
770 let res = sqlx::query(
771 "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)
772 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
773 )
774 .bind(&act.workflow_id)
775 .bind(act.seq)
776 .bind(&act.name)
777 .bind(&act.task_queue)
778 .bind(&act.input)
779 .bind(&act.status)
780 .bind(act.attempt)
781 .bind(act.max_attempts)
782 .bind(act.initial_interval_secs)
783 .bind(act.backoff_coefficient)
784 .bind(act.start_to_close_secs)
785 .bind(act.heartbeat_timeout_secs)
786 .bind(act.scheduled_at)
787 .execute(&self.pool)
788 .await?;
789 Ok(res.last_insert_rowid())
790 }
791
792 async fn get_activity(&self, id: i64) -> Result<Option<WorkflowActivity>> {
793 let row = sqlx::query_as::<_, SqliteActivityRow>(
794 "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
795 FROM workflow.activities WHERE id = ?",
796 )
797 .bind(id)
798 .fetch_optional(&self.pool)
799 .await?;
800 Ok(row.map(Into::into))
801 }
802
803 async fn get_activity_by_workflow_seq(
804 &self,
805 workflow_id: &str,
806 seq: i32,
807 ) -> Result<Option<WorkflowActivity>> {
808 let row = sqlx::query_as::<_, SqliteActivityRow>(
809 "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
810 FROM workflow.activities WHERE workflow_id = ? AND seq = ?",
811 )
812 .bind(workflow_id)
813 .bind(seq)
814 .fetch_optional(&self.pool)
815 .await?;
816 Ok(row.map(Into::into))
817 }
818
819 async fn claim_activity(
820 &self,
821 task_queue: &str,
822 worker_id: &str,
823 ) -> Result<Option<WorkflowActivity>> {
824 let now = timestamp_now();
825 let row = sqlx::query_as::<_, SqliteActivityRow>(
826 "UPDATE workflow.activities SET status = 'RUNNING', claimed_by = ?, started_at = ?
827 WHERE id = (
828 SELECT id FROM workflow.activities
829 WHERE task_queue = ? AND status = 'PENDING'
830 ORDER BY scheduled_at ASC
831 LIMIT 1
832 )
833 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",
834 )
835 .bind(worker_id)
836 .bind(now)
837 .bind(task_queue)
838 .fetch_optional(&self.pool)
839 .await?;
840 Ok(row.map(Into::into))
841 }
842
843 async fn requeue_activity_for_retry(
844 &self,
845 id: i64,
846 next_attempt: i32,
847 next_scheduled_at: f64,
848 ) -> Result<()> {
849 sqlx::query(
850 "UPDATE workflow.activities
851 SET status = 'PENDING', attempt = ?, scheduled_at = ?,
852 claimed_by = NULL, started_at = NULL, last_heartbeat = NULL,
853 error = NULL
854 WHERE id = ?",
855 )
856 .bind(next_attempt)
857 .bind(next_scheduled_at)
858 .bind(id)
859 .execute(&self.pool)
860 .await?;
861 Ok(())
862 }
863
864 async fn complete_activity(
865 &self,
866 id: i64,
867 result: Option<&str>,
868 error: Option<&str>,
869 failed: bool,
870 ) -> Result<()> {
871 let status = if failed { "FAILED" } else { "COMPLETED" };
872 sqlx::query(
873 "UPDATE workflow.activities SET status = ?, result = ?, error = ?, completed_at = ? WHERE id = ?",
874 )
875 .bind(status)
876 .bind(result)
877 .bind(error)
878 .bind(timestamp_now())
879 .bind(id)
880 .execute(&self.pool)
881 .await?;
882 Ok(())
883 }
884
885 async fn heartbeat_activity(&self, id: i64, _details: Option<&str>) -> Result<()> {
886 sqlx::query("UPDATE workflow.activities SET last_heartbeat = ? WHERE id = ?")
887 .bind(timestamp_now())
888 .bind(id)
889 .execute(&self.pool)
890 .await?;
891 Ok(())
892 }
893
894 async fn get_timed_out_activities(&self, now: f64) -> Result<Vec<WorkflowActivity>> {
895 let rows = sqlx::query_as::<_, SqliteActivityRow>(
896 "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
897 FROM workflow.activities
898 WHERE status = 'RUNNING'
899 AND heartbeat_timeout_secs IS NOT NULL
900 AND last_heartbeat IS NOT NULL
901 AND (? - last_heartbeat) > heartbeat_timeout_secs",
902 )
903 .bind(now)
904 .fetch_all(&self.pool)
905 .await?;
906 Ok(rows.into_iter().map(Into::into).collect())
907 }
908
909 async fn create_timer(&self, timer: &WorkflowTimer) -> Result<i64> {
912 let res = sqlx::query(
915 "INSERT OR IGNORE INTO workflow.timers (workflow_id, seq, fire_at, fired) VALUES (?, ?, ?, 0)",
916 )
917 .bind(&timer.workflow_id)
918 .bind(timer.seq)
919 .bind(timer.fire_at)
920 .execute(&self.pool)
921 .await?;
922
923 let id = res.last_insert_rowid();
924 if id != 0 {
925 return Ok(id);
926 }
927
928 let (existing_id,): (i64,) = sqlx::query_as(
930 "SELECT id FROM workflow.timers WHERE workflow_id = ? AND seq = ?",
931 )
932 .bind(&timer.workflow_id)
933 .bind(timer.seq)
934 .fetch_one(&self.pool)
935 .await?;
936 Ok(existing_id)
937 }
938
939 async fn cancel_pending_activities(&self, workflow_id: &str) -> Result<u64> {
940 let res = sqlx::query(
941 "UPDATE workflow.activities SET status = 'CANCELLED', completed_at = ?
942 WHERE workflow_id = ? AND status = 'PENDING'",
943 )
944 .bind(timestamp_now())
945 .bind(workflow_id)
946 .execute(&self.pool)
947 .await?;
948 Ok(res.rows_affected())
949 }
950
951 async fn cancel_pending_timers(&self, workflow_id: &str) -> Result<u64> {
952 let res = sqlx::query(
953 "UPDATE workflow.timers SET fired = 1
954 WHERE workflow_id = ? AND fired = 0",
955 )
956 .bind(workflow_id)
957 .execute(&self.pool)
958 .await?;
959 Ok(res.rows_affected())
960 }
961
962 async fn get_timer_by_workflow_seq(
963 &self,
964 workflow_id: &str,
965 seq: i32,
966 ) -> Result<Option<WorkflowTimer>> {
967 let row = sqlx::query_as::<_, SqliteTimerRow>(
968 "SELECT id, workflow_id, seq, fire_at, fired
969 FROM workflow.timers WHERE workflow_id = ? AND seq = ?",
970 )
971 .bind(workflow_id)
972 .bind(seq)
973 .fetch_optional(&self.pool)
974 .await?;
975 Ok(row.map(Into::into))
976 }
977
978 async fn fire_due_timers(&self, now: f64) -> Result<Vec<WorkflowTimer>> {
979 let rows = sqlx::query_as::<_, SqliteTimerRow>(
980 "UPDATE workflow.timers SET fired = 1
981 WHERE fired = 0 AND fire_at <= ?
982 RETURNING id, workflow_id, seq, fire_at, fired",
983 )
984 .bind(now)
985 .fetch_all(&self.pool)
986 .await?;
987 Ok(rows.into_iter().map(Into::into).collect())
988 }
989
990 async fn send_signal(&self, sig: &WorkflowSignal) -> Result<i64> {
993 let res = sqlx::query(
994 "INSERT INTO workflow.signals (workflow_id, name, payload, consumed, received_at) VALUES (?, ?, ?, 0, ?)",
995 )
996 .bind(&sig.workflow_id)
997 .bind(&sig.name)
998 .bind(&sig.payload)
999 .bind(sig.received_at)
1000 .execute(&self.pool)
1001 .await?;
1002 Ok(res.last_insert_rowid())
1003 }
1004
1005 async fn consume_signals(
1006 &self,
1007 workflow_id: &str,
1008 name: &str,
1009 ) -> Result<Vec<WorkflowSignal>> {
1010 let rows = sqlx::query_as::<_, SqliteSignalRow>(
1011 "UPDATE workflow.signals SET consumed = 1
1012 WHERE workflow_id = ? AND name = ? AND consumed = 0
1013 RETURNING id, workflow_id, name, payload, consumed, received_at",
1014 )
1015 .bind(workflow_id)
1016 .bind(name)
1017 .fetch_all(&self.pool)
1018 .await?;
1019 Ok(rows.into_iter().map(Into::into).collect())
1020 }
1021
1022 async fn create_schedule(&self, sched: &WorkflowSchedule) -> Result<()> {
1025 sqlx::query(
1026 "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)
1027 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1028 )
1029 .bind(&sched.name)
1030 .bind(&sched.namespace)
1031 .bind(&sched.workflow_type)
1032 .bind(&sched.cron_expr)
1033 .bind(&sched.timezone)
1034 .bind(&sched.input)
1035 .bind(&sched.task_queue)
1036 .bind(&sched.overlap_policy)
1037 .bind(sched.paused)
1038 .bind(sched.last_run_at)
1039 .bind(sched.next_run_at)
1040 .bind(&sched.last_workflow_id)
1041 .bind(sched.created_at)
1042 .execute(&self.pool)
1043 .await?;
1044 Ok(())
1045 }
1046
1047 async fn get_schedule(&self, namespace: &str, name: &str) -> Result<Option<WorkflowSchedule>> {
1048 let row = sqlx::query_as::<_, SqliteScheduleRow>(
1049 "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
1050 FROM workflow.schedules WHERE namespace = ? AND name = ?",
1051 )
1052 .bind(namespace)
1053 .bind(name)
1054 .fetch_optional(&self.pool)
1055 .await?;
1056 Ok(row.map(Into::into))
1057 }
1058
1059 async fn list_schedules(&self, namespace: &str) -> Result<Vec<WorkflowSchedule>> {
1060 let rows = sqlx::query_as::<_, SqliteScheduleRow>(
1061 "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
1062 FROM workflow.schedules WHERE namespace = ? ORDER BY name",
1063 )
1064 .bind(namespace)
1065 .fetch_all(&self.pool)
1066 .await?;
1067 Ok(rows.into_iter().map(Into::into).collect())
1068 }
1069
1070 async fn update_schedule_last_run(
1071 &self,
1072 namespace: &str,
1073 name: &str,
1074 last_run_at: f64,
1075 next_run_at: f64,
1076 workflow_id: &str,
1077 ) -> Result<()> {
1078 sqlx::query(
1079 "UPDATE workflow.schedules SET last_run_at = ?, next_run_at = ?, last_workflow_id = ? WHERE namespace = ? AND name = ?",
1080 )
1081 .bind(last_run_at)
1082 .bind(next_run_at)
1083 .bind(workflow_id)
1084 .bind(namespace)
1085 .bind(name)
1086 .execute(&self.pool)
1087 .await?;
1088 Ok(())
1089 }
1090
1091 async fn delete_schedule(&self, namespace: &str, name: &str) -> Result<bool> {
1092 let res =
1093 sqlx::query("DELETE FROM workflow.schedules WHERE namespace = ? AND name = ?")
1094 .bind(namespace)
1095 .bind(name)
1096 .execute(&self.pool)
1097 .await?;
1098 Ok(res.rows_affected() > 0)
1099 }
1100
1101 async fn list_archivable_workflows(
1102 &self,
1103 cutoff: f64,
1104 limit: i64,
1105 ) -> Result<Vec<WorkflowRecord>> {
1106 let rows = sqlx::query_as::<_, SqliteWorkflowRow>(
1107 "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
1108 FROM workflow.workflows
1109 WHERE status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'TIMED_OUT')
1110 AND completed_at IS NOT NULL
1111 AND completed_at < ?
1112 AND archived_at IS NULL
1113 ORDER BY completed_at ASC
1114 LIMIT ?",
1115 )
1116 .bind(cutoff)
1117 .bind(limit)
1118 .fetch_all(&self.pool)
1119 .await?;
1120 Ok(rows.into_iter().map(Into::into).collect())
1121 }
1122
1123 async fn mark_archived_and_purge(
1124 &self,
1125 workflow_id: &str,
1126 archive_uri: &str,
1127 archived_at: f64,
1128 ) -> Result<()> {
1129 let mut tx = self.pool.begin().await?;
1130 sqlx::query("DELETE FROM workflow.events WHERE workflow_id = ?")
1131 .bind(workflow_id)
1132 .execute(&mut *tx)
1133 .await?;
1134 sqlx::query("DELETE FROM workflow.activities WHERE workflow_id = ?")
1135 .bind(workflow_id)
1136 .execute(&mut *tx)
1137 .await?;
1138 sqlx::query("DELETE FROM workflow.timers WHERE workflow_id = ?")
1139 .bind(workflow_id)
1140 .execute(&mut *tx)
1141 .await?;
1142 sqlx::query("DELETE FROM workflow.signals WHERE workflow_id = ?")
1143 .bind(workflow_id)
1144 .execute(&mut *tx)
1145 .await?;
1146 sqlx::query("DELETE FROM workflow.snapshots WHERE workflow_id = ?")
1147 .bind(workflow_id)
1148 .execute(&mut *tx)
1149 .await?;
1150 sqlx::query(
1151 "UPDATE workflow.workflows SET archived_at = ?, archive_uri = ? WHERE id = ?",
1152 )
1153 .bind(archived_at)
1154 .bind(archive_uri)
1155 .bind(workflow_id)
1156 .execute(&mut *tx)
1157 .await?;
1158 tx.commit().await?;
1159 Ok(())
1160 }
1161
1162 async fn upsert_search_attributes(
1163 &self,
1164 workflow_id: &str,
1165 patch_json: &str,
1166 ) -> Result<()> {
1167 let current: Option<(Option<String>,)> =
1170 sqlx::query_as("SELECT search_attributes FROM workflow.workflows WHERE id = ?")
1171 .bind(workflow_id)
1172 .fetch_optional(&self.pool)
1173 .await?;
1174 let merged = merge_search_attrs(
1175 current.and_then(|(s,)| s).as_deref(),
1176 patch_json,
1177 )?;
1178 sqlx::query("UPDATE workflow.workflows SET search_attributes = ? WHERE id = ?")
1179 .bind(merged)
1180 .bind(workflow_id)
1181 .execute(&self.pool)
1182 .await?;
1183 Ok(())
1184 }
1185
1186 async fn update_schedule(
1187 &self,
1188 namespace: &str,
1189 name: &str,
1190 patch: &SchedulePatch,
1191 ) -> Result<Option<WorkflowSchedule>> {
1192 let mut sets: Vec<&'static str> = Vec::new();
1195 if patch.cron_expr.is_some() {
1196 sets.push("cron_expr = ?");
1197 }
1198 if patch.timezone.is_some() {
1199 sets.push("timezone = ?");
1200 }
1201 if patch.input.is_some() {
1202 sets.push("input = ?");
1203 }
1204 if patch.task_queue.is_some() {
1205 sets.push("task_queue = ?");
1206 }
1207 if patch.overlap_policy.is_some() {
1208 sets.push("overlap_policy = ?");
1209 }
1210 if sets.is_empty() {
1212 return self.get_schedule(namespace, name).await;
1213 }
1214
1215 let sql = format!(
1216 "UPDATE workflow.schedules SET {} WHERE namespace = ? AND name = ?",
1217 sets.join(", ")
1218 );
1219 let mut q = sqlx::query(&sql);
1220 if let Some(ref v) = patch.cron_expr {
1221 q = q.bind(v);
1222 }
1223 if let Some(ref v) = patch.timezone {
1224 q = q.bind(v);
1225 }
1226 if let Some(ref v) = patch.input {
1227 q = q.bind(v.to_string());
1228 }
1229 if let Some(ref v) = patch.task_queue {
1230 q = q.bind(v);
1231 }
1232 if let Some(ref v) = patch.overlap_policy {
1233 q = q.bind(v);
1234 }
1235 let res = q
1236 .bind(namespace)
1237 .bind(name)
1238 .execute(&self.pool)
1239 .await?;
1240 if res.rows_affected() == 0 {
1241 return Ok(None);
1242 }
1243 self.get_schedule(namespace, name).await
1244 }
1245
1246 async fn set_schedule_paused(
1247 &self,
1248 namespace: &str,
1249 name: &str,
1250 paused: bool,
1251 ) -> Result<Option<WorkflowSchedule>> {
1252 let res = sqlx::query(
1253 "UPDATE workflow.schedules SET paused = ? WHERE namespace = ? AND name = ?",
1254 )
1255 .bind(paused)
1256 .bind(namespace)
1257 .bind(name)
1258 .execute(&self.pool)
1259 .await?;
1260 if res.rows_affected() == 0 {
1261 return Ok(None);
1262 }
1263 self.get_schedule(namespace, name).await
1264 }
1265
1266 async fn register_worker(&self, w: &WorkflowWorker) -> Result<()> {
1269 sqlx::query(
1270 "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)
1271 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1272 )
1273 .bind(&w.id)
1274 .bind(&w.namespace)
1275 .bind(&w.identity)
1276 .bind(&w.task_queue)
1277 .bind(&w.workflows)
1278 .bind(&w.activities)
1279 .bind(w.max_concurrent_workflows)
1280 .bind(w.max_concurrent_activities)
1281 .bind(w.active_tasks)
1282 .bind(w.last_heartbeat)
1283 .bind(w.registered_at)
1284 .execute(&self.pool)
1285 .await?;
1286 Ok(())
1287 }
1288
1289 async fn heartbeat_worker(&self, id: &str, now: f64) -> Result<()> {
1290 sqlx::query("UPDATE workflow.workers SET last_heartbeat = ? WHERE id = ?")
1291 .bind(now)
1292 .bind(id)
1293 .execute(&self.pool)
1294 .await?;
1295 Ok(())
1296 }
1297
1298 async fn list_workers(&self, namespace: &str) -> Result<Vec<WorkflowWorker>> {
1299 let rows = sqlx::query_as::<_, SqliteWorkerRow>(
1300 "SELECT id, namespace, identity, task_queue, workflows, activities, max_concurrent_workflows, max_concurrent_activities, active_tasks, last_heartbeat, registered_at
1301 FROM workflow.workers WHERE namespace = ? ORDER BY registered_at",
1302 )
1303 .bind(namespace)
1304 .fetch_all(&self.pool)
1305 .await?;
1306 Ok(rows.into_iter().map(Into::into).collect())
1307 }
1308
1309 async fn remove_dead_workers(&self, cutoff: f64) -> Result<Vec<String>> {
1310 let rows: Vec<(String,)> =
1311 sqlx::query_as("SELECT id FROM workflow.workers WHERE last_heartbeat < ?")
1312 .bind(cutoff)
1313 .fetch_all(&self.pool)
1314 .await?;
1315 let ids: Vec<String> = rows.into_iter().map(|r| r.0).collect();
1316 if !ids.is_empty() {
1317 sqlx::query("DELETE FROM workflow.workers WHERE last_heartbeat < ?")
1318 .bind(cutoff)
1319 .execute(&self.pool)
1320 .await?;
1321 }
1322 Ok(ids)
1323 }
1324
1325 async fn list_child_workflows(&self, parent_id: &str) -> Result<Vec<WorkflowRecord>> {
1328 let rows = sqlx::query_as::<_, SqliteWorkflowRow>(
1329 "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
1330 FROM workflow.workflows WHERE parent_id = ? ORDER BY created_at ASC",
1331 )
1332 .bind(parent_id)
1333 .fetch_all(&self.pool)
1334 .await?;
1335 Ok(rows.into_iter().map(Into::into).collect())
1336 }
1337
1338 async fn create_snapshot(
1341 &self,
1342 workflow_id: &str,
1343 event_seq: i32,
1344 state_json: &str,
1345 ) -> Result<()> {
1346 sqlx::query(
1347 "INSERT OR REPLACE INTO workflow.snapshots (workflow_id, event_seq, state_json, created_at)
1348 VALUES (?, ?, ?, ?)",
1349 )
1350 .bind(workflow_id)
1351 .bind(event_seq)
1352 .bind(state_json)
1353 .bind(timestamp_now())
1354 .execute(&self.pool)
1355 .await?;
1356 Ok(())
1357 }
1358
1359 async fn get_latest_snapshot(
1360 &self,
1361 workflow_id: &str,
1362 ) -> Result<Option<WorkflowSnapshot>> {
1363 let row = sqlx::query_as::<_, (String, i32, String, f64)>(
1364 "SELECT workflow_id, event_seq, state_json, created_at
1365 FROM workflow.snapshots WHERE workflow_id = ?
1366 ORDER BY event_seq DESC LIMIT 1",
1367 )
1368 .bind(workflow_id)
1369 .fetch_optional(&self.pool)
1370 .await?;
1371
1372 Ok(row.map(|(workflow_id, event_seq, state_json, created_at)| WorkflowSnapshot {
1373 workflow_id,
1374 event_seq,
1375 state_json,
1376 created_at,
1377 }))
1378 }
1379
1380 async fn get_queue_stats(&self, namespace: &str) -> Result<Vec<QueueStats>> {
1383 let rows = sqlx::query_as::<_, (String, i64, i64)>(
1385 "SELECT a.task_queue,
1386 SUM(CASE WHEN a.status = 'PENDING' THEN 1 ELSE 0 END),
1387 SUM(CASE WHEN a.status = 'RUNNING' THEN 1 ELSE 0 END)
1388 FROM workflow.activities a
1389 INNER JOIN workflow.workflows w ON w.id = a.workflow_id
1390 WHERE w.namespace = ?
1391 GROUP BY a.task_queue",
1392 )
1393 .bind(namespace)
1394 .fetch_all(&self.pool)
1395 .await?;
1396
1397 let mut stats: Vec<QueueStats> = rows
1398 .into_iter()
1399 .map(|(queue, pending, running)| QueueStats {
1400 queue,
1401 pending_activities: pending,
1402 running_activities: running,
1403 workers: 0,
1404 })
1405 .collect();
1406
1407 let worker_rows = sqlx::query_as::<_, (String, i64)>(
1409 "SELECT task_queue, COUNT(*) FROM workflow.workers WHERE namespace = ? GROUP BY task_queue",
1410 )
1411 .bind(namespace)
1412 .fetch_all(&self.pool)
1413 .await?;
1414
1415 for (queue, count) in worker_rows {
1416 if let Some(s) = stats.iter_mut().find(|s| s.queue == queue) {
1417 s.workers = count;
1418 } else {
1419 stats.push(QueueStats {
1420 queue,
1421 pending_activities: 0,
1422 running_activities: 0,
1423 workers: count,
1424 });
1425 }
1426 }
1427
1428 stats.sort_by(|a, b| a.queue.cmp(&b.queue));
1429 Ok(stats)
1430 }
1431
1432 async fn try_acquire_scheduler_lock(&self) -> Result<bool> {
1435 self.refresh_engine_lock().await.ok();
1438 Ok(true)
1439 }
1440
1441}
1442
1443fn timestamp_now() -> f64 {
1444 std::time::SystemTime::now()
1445 .duration_since(std::time::UNIX_EPOCH)
1446 .unwrap()
1447 .as_secs_f64()
1448}
1449
1450pub(crate) fn merge_search_attrs(current: Option<&str>, patch_json: &str) -> Result<String> {
1453 let mut current_map: serde_json::Map<String, serde_json::Value> = current
1454 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1455 .and_then(|v| v.as_object().cloned())
1456 .unwrap_or_default();
1457 let patch: serde_json::Value = serde_json::from_str(patch_json)
1458 .map_err(|e| anyhow::anyhow!("invalid search_attributes patch: {e}"))?;
1459 let patch_obj = patch
1460 .as_object()
1461 .ok_or_else(|| anyhow::anyhow!("search_attributes patch must be a JSON object"))?;
1462 for (k, v) in patch_obj {
1463 current_map.insert(k.clone(), v.clone());
1464 }
1465 Ok(serde_json::Value::Object(current_map).to_string())
1466}
1467
1468#[derive(sqlx::FromRow)]
1471struct SqliteWorkflowRow {
1472 id: String,
1473 namespace: String,
1474 run_id: String,
1475 workflow_type: String,
1476 task_queue: String,
1477 status: String,
1478 input: Option<String>,
1479 result: Option<String>,
1480 error: Option<String>,
1481 parent_id: Option<String>,
1482 claimed_by: Option<String>,
1483 search_attributes: Option<String>,
1484 archived_at: Option<f64>,
1485 archive_uri: Option<String>,
1486 created_at: f64,
1487 updated_at: f64,
1488 completed_at: Option<f64>,
1489}
1490
1491impl From<SqliteWorkflowRow> for WorkflowRecord {
1492 fn from(r: SqliteWorkflowRow) -> Self {
1493 Self {
1494 id: r.id,
1495 namespace: r.namespace,
1496 run_id: r.run_id,
1497 workflow_type: r.workflow_type,
1498 task_queue: r.task_queue,
1499 status: r.status,
1500 input: r.input,
1501 result: r.result,
1502 error: r.error,
1503 parent_id: r.parent_id,
1504 claimed_by: r.claimed_by,
1505 search_attributes: r.search_attributes,
1506 archived_at: r.archived_at,
1507 archive_uri: r.archive_uri,
1508 created_at: r.created_at,
1509 updated_at: r.updated_at,
1510 completed_at: r.completed_at,
1511 }
1512 }
1513}
1514
1515#[derive(sqlx::FromRow)]
1516struct SqliteEventRow {
1517 id: i64,
1518 workflow_id: String,
1519 seq: i32,
1520 event_type: String,
1521 payload: Option<String>,
1522 timestamp: f64,
1523}
1524
1525impl From<SqliteEventRow> for WorkflowEvent {
1526 fn from(r: SqliteEventRow) -> Self {
1527 Self {
1528 id: Some(r.id),
1529 workflow_id: r.workflow_id,
1530 seq: r.seq,
1531 event_type: r.event_type,
1532 payload: r.payload,
1533 timestamp: r.timestamp,
1534 }
1535 }
1536}
1537
1538#[derive(sqlx::FromRow)]
1539struct SqliteActivityRow {
1540 id: i64,
1541 workflow_id: String,
1542 seq: i32,
1543 name: String,
1544 task_queue: String,
1545 input: Option<String>,
1546 status: String,
1547 result: Option<String>,
1548 error: Option<String>,
1549 attempt: i32,
1550 max_attempts: i32,
1551 initial_interval_secs: f64,
1552 backoff_coefficient: f64,
1553 start_to_close_secs: f64,
1554 heartbeat_timeout_secs: Option<f64>,
1555 claimed_by: Option<String>,
1556 scheduled_at: f64,
1557 started_at: Option<f64>,
1558 completed_at: Option<f64>,
1559 last_heartbeat: Option<f64>,
1560}
1561
1562impl From<SqliteActivityRow> for WorkflowActivity {
1563 fn from(r: SqliteActivityRow) -> Self {
1564 Self {
1565 id: Some(r.id),
1566 workflow_id: r.workflow_id,
1567 seq: r.seq,
1568 name: r.name,
1569 task_queue: r.task_queue,
1570 input: r.input,
1571 status: r.status,
1572 result: r.result,
1573 error: r.error,
1574 attempt: r.attempt,
1575 max_attempts: r.max_attempts,
1576 initial_interval_secs: r.initial_interval_secs,
1577 backoff_coefficient: r.backoff_coefficient,
1578 start_to_close_secs: r.start_to_close_secs,
1579 heartbeat_timeout_secs: r.heartbeat_timeout_secs,
1580 claimed_by: r.claimed_by,
1581 scheduled_at: r.scheduled_at,
1582 started_at: r.started_at,
1583 completed_at: r.completed_at,
1584 last_heartbeat: r.last_heartbeat,
1585 }
1586 }
1587}
1588
1589#[derive(sqlx::FromRow)]
1590struct SqliteTimerRow {
1591 id: i64,
1592 workflow_id: String,
1593 seq: i32,
1594 fire_at: f64,
1595 fired: bool,
1596}
1597
1598impl From<SqliteTimerRow> for WorkflowTimer {
1599 fn from(r: SqliteTimerRow) -> Self {
1600 Self {
1601 id: Some(r.id),
1602 workflow_id: r.workflow_id,
1603 seq: r.seq,
1604 fire_at: r.fire_at,
1605 fired: r.fired,
1606 }
1607 }
1608}
1609
1610#[derive(sqlx::FromRow)]
1611struct SqliteSignalRow {
1612 id: i64,
1613 workflow_id: String,
1614 name: String,
1615 payload: Option<String>,
1616 consumed: bool,
1617 received_at: f64,
1618}
1619
1620impl From<SqliteSignalRow> for WorkflowSignal {
1621 fn from(r: SqliteSignalRow) -> Self {
1622 Self {
1623 id: Some(r.id),
1624 workflow_id: r.workflow_id,
1625 name: r.name,
1626 payload: r.payload,
1627 consumed: r.consumed,
1628 received_at: r.received_at,
1629 }
1630 }
1631}
1632
1633#[derive(sqlx::FromRow)]
1634struct SqliteScheduleRow {
1635 name: String,
1636 namespace: String,
1637 workflow_type: String,
1638 cron_expr: String,
1639 timezone: String,
1640 input: Option<String>,
1641 task_queue: String,
1642 overlap_policy: String,
1643 paused: bool,
1644 last_run_at: Option<f64>,
1645 next_run_at: Option<f64>,
1646 last_workflow_id: Option<String>,
1647 created_at: f64,
1648}
1649
1650impl From<SqliteScheduleRow> for WorkflowSchedule {
1651 fn from(r: SqliteScheduleRow) -> Self {
1652 Self {
1653 name: r.name,
1654 namespace: r.namespace,
1655 workflow_type: r.workflow_type,
1656 cron_expr: r.cron_expr,
1657 timezone: r.timezone,
1658 input: r.input,
1659 task_queue: r.task_queue,
1660 overlap_policy: r.overlap_policy,
1661 paused: r.paused,
1662 last_run_at: r.last_run_at,
1663 next_run_at: r.next_run_at,
1664 last_workflow_id: r.last_workflow_id,
1665 created_at: r.created_at,
1666 }
1667 }
1668}
1669
1670#[derive(sqlx::FromRow)]
1671struct SqliteWorkerRow {
1672 id: String,
1673 namespace: String,
1674 identity: String,
1675 task_queue: String,
1676 workflows: Option<String>,
1677 activities: Option<String>,
1678 max_concurrent_workflows: i32,
1679 max_concurrent_activities: i32,
1680 active_tasks: i32,
1681 last_heartbeat: f64,
1682 registered_at: f64,
1683}
1684
1685impl From<SqliteWorkerRow> for WorkflowWorker {
1686 fn from(r: SqliteWorkerRow) -> Self {
1687 Self {
1688 id: r.id,
1689 namespace: r.namespace,
1690 identity: r.identity,
1691 task_queue: r.task_queue,
1692 workflows: r.workflows,
1693 activities: r.activities,
1694 max_concurrent_workflows: r.max_concurrent_workflows,
1695 max_concurrent_activities: r.max_concurrent_activities,
1696 active_tasks: r.active_tasks,
1697 last_heartbeat: r.last_heartbeat,
1698 registered_at: r.registered_at,
1699 }
1700 }
1701}