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