1use aion_store::{
9 ClaimScope, DEFAULT_OUTBOX_ROUTE, OutboxRow, OutboxStatus, Payload, RunId, StoreError,
10 WorkflowId,
11};
12use chrono::{DateTime, SecondsFormat, Utc};
13use libsql::{Connection, Row, Transaction, TransactionBehavior, params};
14
15mod transitions;
16pub(crate) use transitions::{
17 complete_outbox_row, fail_outbox_row, retry_outbox_row, settle_outbox_row_cancelled,
18};
19
20const INSERT_OUTBOX_SQL: &str = "
21INSERT OR IGNORE INTO outbox
22 (dispatch_key, workflow_id, ordinal, activity_type, input, status, attempt, visible_after, claimed_at, run_id, namespace, task_queue, node)
23VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)";
24
25const REARM_OUTBOX_SQL: &str = "
26INSERT INTO outbox
27 (dispatch_key, workflow_id, ordinal, activity_type, input, status, attempt, visible_after, claimed_at, namespace, task_queue, node)
28VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, ?9, ?10, ?11)
29ON CONFLICT(dispatch_key) DO UPDATE SET status = 'pending', visible_after = ?8, claimed_at = NULL";
30
31const SELECT_CLAIMABLE_SQL: &str = "
32SELECT dispatch_key, workflow_id, ordinal, activity_type, input, status, attempt, visible_after, claimed_at, run_id, namespace, task_queue, node
33FROM outbox
34WHERE status = 'pending' AND visible_after <= ?1
35ORDER BY visible_after ASC, dispatch_key ASC
36LIMIT ?2";
37
38const SELECT_CLAIMABLE_UNBOUNDED_SQL: &str = "
45SELECT dispatch_key, workflow_id, ordinal, activity_type, input, status, attempt, visible_after, claimed_at, run_id, namespace, task_queue, node
46FROM outbox
47WHERE status = 'pending' AND visible_after <= ?1
48ORDER BY visible_after ASC, dispatch_key ASC";
49
50const SELECT_CLAIMABLE_SCOPED_PREFIX_SQL: &str = "
55SELECT dispatch_key, workflow_id, ordinal, activity_type, input, status, attempt, visible_after, claimed_at, run_id, namespace, task_queue, node
56FROM outbox
57WHERE status = 'pending' AND visible_after <= ?1 AND namespace = ?3 AND task_queue = ?4";
58
59const SELECT_CLAIMABLE_SCOPED_SUFFIX_SQL: &str = "
60ORDER BY visible_after ASC, dispatch_key ASC
61LIMIT ?2";
62
63const SELECT_CLAIMABLE_SCOPED_SUFFIX_UNBOUNDED_SQL: &str = "
67ORDER BY visible_after ASC, dispatch_key ASC";
68
69const NODE_CLAUSE_PINNED_OR_UNPINNED: &str = " AND (node IS NULL OR node = ?5)";
71const NODE_CLAUSE_UNPINNED_ONLY: &str = " AND node IS NULL";
72
73const CLAIM_ROW_SQL: &str = "
74UPDATE outbox SET status = 'claimed', claimed_at = ?2 WHERE dispatch_key = ?1 AND status = 'pending'";
75
76const COUNT_INFLIGHT_OUTBOX_SQL: &str = "
80SELECT COUNT(*) FROM outbox WHERE namespace = ?1 AND status IN ('pending', 'claimed')";
81
82const COUNT_CLAIMED_OUTBOX_SQL: &str = "
86SELECT COUNT(*) FROM outbox WHERE namespace = ?1 AND status = 'claimed'";
87
88const COUNT_CLAIMED_OUTBOX_BY_NAMESPACE_SQL: &str = "
93SELECT namespace, COUNT(*) FROM outbox WHERE status = 'claimed' GROUP BY namespace";
94
95const PENDING_OUTBOX_ROUTES_SQL: &str = "
100SELECT DISTINCT namespace, task_queue, node
101FROM outbox
102WHERE status = 'pending' AND visible_after <= ?1";
103
104const SELECT_STALE_CLAIMED_SQL: &str = "
105SELECT dispatch_key, workflow_id, ordinal, activity_type, input, status, attempt, visible_after, claimed_at, run_id, namespace, task_queue, node
106FROM outbox
107WHERE status = 'claimed' AND claimed_at IS NOT NULL AND claimed_at < ?1
108ORDER BY claimed_at ASC, dispatch_key ASC
109LIMIT ?2";
110
111const SELECT_LIVE_KEYS_FOR_WORKFLOW_SQL: &str = "
115SELECT dispatch_key FROM outbox
116WHERE workflow_id = ?1 AND status IN ('pending', 'claimed')
117ORDER BY dispatch_key ASC";
118
119const SETTLE_WORKFLOW_LIVE_ROWS_SQL: &str = "
120UPDATE outbox SET status = 'cancelled', claimed_at = NULL
121WHERE workflow_id = ?1 AND status IN ('pending', 'claimed')";
122
123const SELECT_UNSETTLED_WORKFLOW_IDS_SQL: &str = "
126SELECT DISTINCT workflow_id FROM outbox
127WHERE status IN ('pending', 'claimed')
128ORDER BY workflow_id ASC";
129
130const REARM_STALE_CLAIMED_ROW_SQL: &str = "
131UPDATE outbox SET status = 'pending', visible_after = ?2, claimed_at = NULL
132WHERE dispatch_key = ?1 AND status = 'claimed'";
133
134pub(crate) async fn insert_outbox_row(tx: &Transaction, row: &OutboxRow) -> Result<(), StoreError> {
144 let input = encode_payload(&row.input)?;
145 tx.execute(
146 INSERT_OUTBOX_SQL,
147 params![
148 row.dispatch_key.clone(),
149 row.workflow_id.to_string(),
150 i64::try_from(row.ordinal).map_err(|_| StoreError::Backend(format!(
151 "outbox ordinal overflow: {}",
152 row.ordinal
153 )))?,
154 row.activity_type.clone(),
155 input,
156 row.status.as_str(),
157 i64::from(row.attempt),
158 encode_instant(row.visible_after),
159 row.claimed_at.map(encode_instant),
160 row.run_id.as_ref().map(ToString::to_string),
161 row.namespace.clone(),
162 row.task_queue.clone(),
163 row.node.clone()
164 ],
165 )
166 .await
167 .map(|_| ())
168 .map_err(|error| crate::error::libsql_error(&error))
169}
170
171pub(crate) async fn append_outbox_batch(
178 conn: &Connection,
179 rows: &[OutboxRow],
180) -> Result<(), StoreError> {
181 if rows.is_empty() {
182 return Ok(());
183 }
184
185 let tx = conn
186 .transaction_with_behavior(TransactionBehavior::Immediate)
187 .await
188 .map_err(|error| crate::error::libsql_error(&error))?;
189
190 for row in rows {
191 if let Err(error) = insert_outbox_row(&tx, row).await {
192 rollback(tx).await?;
193 return Err(error);
194 }
195 }
196
197 tx.commit()
198 .await
199 .map_err(|error| crate::error::libsql_error(&error))
200}
201
202pub(crate) async fn rearm_outbox_pending(
215 conn: &Connection,
216 rows: &[OutboxRow],
217) -> Result<(), StoreError> {
218 if rows.is_empty() {
219 return Ok(());
220 }
221
222 let tx = conn
223 .transaction_with_behavior(TransactionBehavior::Immediate)
224 .await
225 .map_err(|error| crate::error::libsql_error(&error))?;
226
227 for row in rows {
228 if let Err(error) = rearm_outbox_row(&tx, row).await {
229 rollback(tx).await?;
230 return Err(error);
231 }
232 }
233
234 tx.commit()
235 .await
236 .map_err(|error| crate::error::libsql_error(&error))
237}
238
239async fn rearm_outbox_row(tx: &Transaction, row: &OutboxRow) -> Result<(), StoreError> {
240 let input = encode_payload(&row.input)?;
241 tx.execute(
242 REARM_OUTBOX_SQL,
243 params![
244 row.dispatch_key.clone(),
245 row.workflow_id.to_string(),
246 i64::try_from(row.ordinal).map_err(|_| StoreError::Backend(format!(
247 "outbox ordinal overflow: {}",
248 row.ordinal
249 )))?,
250 row.activity_type.clone(),
251 input,
252 OutboxStatus::Pending.as_str(),
253 i64::from(row.attempt),
254 encode_instant(row.visible_after),
255 row.namespace.clone(),
256 row.task_queue.clone(),
257 row.node.clone()
258 ],
259 )
260 .await
261 .map(|_| ())
262 .map_err(|error| crate::error::libsql_error(&error))
263}
264
265pub(crate) async fn claim_outbox_rows(
272 conn: &Connection,
273 limit: u32,
274) -> Result<Vec<OutboxRow>, StoreError> {
275 if limit == 0 {
276 return Ok(Vec::new());
277 }
278
279 let tx = conn
280 .transaction_with_behavior(TransactionBehavior::Immediate)
281 .await
282 .map_err(|error| crate::error::libsql_error(&error))?;
283
284 let claimed = match select_and_claim(&tx, limit).await {
285 Ok(claimed) => claimed,
286 Err(error) => {
287 rollback(tx).await?;
288 return Err(error);
289 }
290 };
291
292 tx.commit()
293 .await
294 .map_err(|error| crate::error::libsql_error(&error))?;
295
296 Ok(claimed)
297}
298
299pub(crate) async fn claim_outbox_rows_excluding(
311 conn: &Connection,
312 limit: u32,
313 held: &std::collections::HashSet<aion_core::WorkflowId>,
314) -> Result<Vec<OutboxRow>, StoreError> {
315 if limit == 0 {
316 return Ok(Vec::new());
317 }
318 if held.is_empty() {
319 return claim_outbox_rows(conn, limit).await;
320 }
321
322 let tx = conn
323 .transaction_with_behavior(TransactionBehavior::Immediate)
324 .await
325 .map_err(|error| crate::error::libsql_error(&error))?;
326
327 let claimed = match select_and_claim_excluding(&tx, limit, held).await {
328 Ok(claimed) => claimed,
329 Err(error) => {
330 rollback(tx).await?;
331 return Err(error);
332 }
333 };
334
335 tx.commit()
336 .await
337 .map_err(|error| crate::error::libsql_error(&error))?;
338
339 Ok(claimed)
340}
341
342async fn select_and_claim_excluding(
343 tx: &Transaction,
344 limit: u32,
345 held: &std::collections::HashSet<aion_core::WorkflowId>,
346) -> Result<Vec<OutboxRow>, StoreError> {
347 let claimed_at = Utc::now();
348 let now = encode_instant(claimed_at);
349 let mut rows = tx
353 .query(SELECT_CLAIMABLE_UNBOUNDED_SQL, params![now.clone()])
354 .await
355 .map_err(|error| crate::error::libsql_error(&error))?;
356
357 let mut claimed = Vec::new();
358 while let Some(row) = rows
359 .next()
360 .await
361 .map_err(|error| crate::error::libsql_error(&error))?
362 {
363 if claimed.len() >= limit as usize {
364 break;
365 }
366 let decoded = decode_row(&row)?;
367 if held.contains(&decoded.workflow_id) {
370 continue;
371 }
372 tx.execute(
373 CLAIM_ROW_SQL,
374 params![decoded.dispatch_key.clone(), now.clone()],
375 )
376 .await
377 .map_err(|error| crate::error::libsql_error(&error))?;
378 claimed.push(OutboxRow {
379 status: OutboxStatus::Claimed,
380 claimed_at: Some(claimed_at),
381 ..decoded
382 });
383 }
384
385 Ok(claimed)
386}
387
388async fn select_and_claim(tx: &Transaction, limit: u32) -> Result<Vec<OutboxRow>, StoreError> {
389 let claimed_at = Utc::now();
390 let now = encode_instant(claimed_at);
391 let mut rows = tx
392 .query(SELECT_CLAIMABLE_SQL, params![now.clone(), i64::from(limit)])
393 .await
394 .map_err(|error| crate::error::libsql_error(&error))?;
395
396 let mut claimed = Vec::new();
397 while let Some(row) = rows
398 .next()
399 .await
400 .map_err(|error| crate::error::libsql_error(&error))?
401 {
402 let decoded = decode_row(&row)?;
403 tx.execute(
404 CLAIM_ROW_SQL,
405 params![decoded.dispatch_key.clone(), now.clone()],
406 )
407 .await
408 .map_err(|error| crate::error::libsql_error(&error))?;
409 claimed.push(OutboxRow {
410 status: OutboxStatus::Claimed,
411 claimed_at: Some(claimed_at),
412 ..decoded
413 });
414 }
415
416 Ok(claimed)
417}
418
419pub(crate) async fn claim_outbox_rows_scoped(
429 conn: &Connection,
430 scope: &ClaimScope,
431 limit: u32,
432) -> Result<Vec<OutboxRow>, StoreError> {
433 claim_outbox_rows_scoped_excluding(conn, scope, limit, &std::collections::HashSet::new()).await
434}
435
436pub(crate) async fn claim_outbox_rows_scoped_excluding(
449 conn: &Connection,
450 scope: &ClaimScope,
451 limit: u32,
452 held: &std::collections::HashSet<aion_core::WorkflowId>,
453) -> Result<Vec<OutboxRow>, StoreError> {
454 if limit == 0 {
455 return Ok(Vec::new());
456 }
457
458 let tx = conn
459 .transaction_with_behavior(TransactionBehavior::Immediate)
460 .await
461 .map_err(|error| crate::error::libsql_error(&error))?;
462
463 let claimed = match select_and_claim_scoped(&tx, scope, limit, held).await {
464 Ok(claimed) => claimed,
465 Err(error) => {
466 rollback(tx).await?;
467 return Err(error);
468 }
469 };
470
471 tx.commit()
472 .await
473 .map_err(|error| crate::error::libsql_error(&error))?;
474
475 Ok(claimed)
476}
477
478async fn select_and_claim_scoped(
479 tx: &Transaction,
480 scope: &ClaimScope,
481 limit: u32,
482 held: &std::collections::HashSet<aion_core::WorkflowId>,
483) -> Result<Vec<OutboxRow>, StoreError> {
484 let claimed_at = Utc::now();
485 let now = encode_instant(claimed_at);
486
487 let node_clause = if scope.node.is_some() {
490 NODE_CLAUSE_PINNED_OR_UNPINNED
491 } else {
492 NODE_CLAUSE_UNPINNED_ONLY
493 };
494 let suffix = if held.is_empty() {
498 SELECT_CLAIMABLE_SCOPED_SUFFIX_SQL
499 } else {
500 SELECT_CLAIMABLE_SCOPED_SUFFIX_UNBOUNDED_SQL
501 };
502 let sql = format!("{SELECT_CLAIMABLE_SCOPED_PREFIX_SQL}{node_clause}{suffix}");
503
504 let mut rows = if let Some(node) = scope.node.as_deref() {
506 tx.query(
507 &sql,
508 params![
509 now.clone(),
510 i64::from(limit),
511 scope.namespace.clone(),
512 scope.task_queue.clone(),
513 node.to_string()
514 ],
515 )
516 .await
517 } else {
518 tx.query(
519 &sql,
520 params![
521 now.clone(),
522 i64::from(limit),
523 scope.namespace.clone(),
524 scope.task_queue.clone()
525 ],
526 )
527 .await
528 }
529 .map_err(|error| crate::error::libsql_error(&error))?;
530
531 let mut claimed = Vec::new();
532 while let Some(row) = rows
533 .next()
534 .await
535 .map_err(|error| crate::error::libsql_error(&error))?
536 {
537 if claimed.len() >= limit as usize {
538 break;
539 }
540 let decoded = decode_row(&row)?;
541 if held.contains(&decoded.workflow_id) {
546 continue;
547 }
548 tx.execute(
549 CLAIM_ROW_SQL,
550 params![decoded.dispatch_key.clone(), now.clone()],
551 )
552 .await
553 .map_err(|error| crate::error::libsql_error(&error))?;
554 claimed.push(OutboxRow {
555 status: OutboxStatus::Claimed,
556 claimed_at: Some(claimed_at),
557 ..decoded
558 });
559 }
560
561 Ok(claimed)
562}
563
564pub(crate) async fn rearm_stale_claimed_outbox_rows(
574 conn: &Connection,
575 older_than: DateTime<Utc>,
576 visible_after: DateTime<Utc>,
577 limit: u32,
578) -> Result<Vec<OutboxRow>, StoreError> {
579 if limit == 0 {
580 return Ok(Vec::new());
581 }
582
583 let tx = conn
584 .transaction_with_behavior(TransactionBehavior::Immediate)
585 .await
586 .map_err(|error| crate::error::libsql_error(&error))?;
587
588 let rows = match select_and_rearm_stale_claimed(&tx, older_than, visible_after, limit).await {
589 Ok(rows) => rows,
590 Err(error) => {
591 rollback(tx).await?;
592 return Err(error);
593 }
594 };
595
596 tx.commit()
597 .await
598 .map_err(|error| crate::error::libsql_error(&error))?;
599
600 Ok(rows)
601}
602
603async fn select_and_rearm_stale_claimed(
604 tx: &Transaction,
605 older_than: DateTime<Utc>,
606 visible_after: DateTime<Utc>,
607 limit: u32,
608) -> Result<Vec<OutboxRow>, StoreError> {
609 let mut rows = tx
610 .query(
611 SELECT_STALE_CLAIMED_SQL,
612 params![encode_instant(older_than), i64::from(limit)],
613 )
614 .await
615 .map_err(|error| crate::error::libsql_error(&error))?;
616
617 let mut rearmed = Vec::new();
618 while let Some(row) = rows
619 .next()
620 .await
621 .map_err(|error| crate::error::libsql_error(&error))?
622 {
623 let decoded = decode_row(&row)?;
624 tx.execute(
625 REARM_STALE_CLAIMED_ROW_SQL,
626 params![decoded.dispatch_key.clone(), encode_instant(visible_after)],
627 )
628 .await
629 .map_err(|error| crate::error::libsql_error(&error))?;
630 rearmed.push(OutboxRow {
631 status: OutboxStatus::Pending,
632 visible_after,
633 claimed_at: None,
634 ..decoded
635 });
636 }
637
638 Ok(rearmed)
639}
640
641pub(crate) async fn list_stale_claimed_outbox_rows(
653 conn: &Connection,
654 older_than: DateTime<Utc>,
655 limit: u32,
656) -> Result<Vec<OutboxRow>, StoreError> {
657 if limit == 0 {
658 return Ok(Vec::new());
659 }
660 let mut rows = conn
661 .query(
662 SELECT_STALE_CLAIMED_SQL,
663 params![encode_instant(older_than), i64::from(limit)],
664 )
665 .await
666 .map_err(|error| crate::error::libsql_error(&error))?;
667 let mut stale = Vec::new();
668 while let Some(row) = rows
669 .next()
670 .await
671 .map_err(|error| crate::error::libsql_error(&error))?
672 {
673 stale.push(decode_row(&row)?);
674 }
675 Ok(stale)
676}
677
678pub(crate) async fn list_unsettled_outbox_workflow_ids(
685 conn: &Connection,
686) -> Result<Vec<WorkflowId>, StoreError> {
687 let mut rows = conn
688 .query(SELECT_UNSETTLED_WORKFLOW_IDS_SQL, ())
689 .await
690 .map_err(|error| crate::error::libsql_error(&error))?;
691 let mut workflow_ids = Vec::new();
692 while let Some(row) = rows
693 .next()
694 .await
695 .map_err(|error| crate::error::libsql_error(&error))?
696 {
697 let workflow_id: String = row
698 .get(0)
699 .map_err(|error| crate::error::libsql_error(&error))?;
700 workflow_ids.push(decode_workflow_id(&workflow_id)?);
701 }
702 Ok(workflow_ids)
703}
704
705pub(crate) async fn settle_workflow_outbox_rows_cancelled(
716 conn: &Connection,
717 workflow_id: &WorkflowId,
718) -> Result<Vec<String>, StoreError> {
719 let tx = conn
720 .transaction_with_behavior(TransactionBehavior::Immediate)
721 .await
722 .map_err(|error| crate::error::libsql_error(&error))?;
723 let settled = match select_and_settle_workflow_rows(&tx, workflow_id).await {
724 Ok(settled) => settled,
725 Err(error) => {
726 rollback(tx).await?;
727 return Err(error);
728 }
729 };
730 tx.commit()
731 .await
732 .map_err(|error| crate::error::libsql_error(&error))?;
733 Ok(settled)
734}
735
736async fn select_and_settle_workflow_rows(
737 tx: &Transaction,
738 workflow_id: &WorkflowId,
739) -> Result<Vec<String>, StoreError> {
740 let mut rows = tx
741 .query(
742 SELECT_LIVE_KEYS_FOR_WORKFLOW_SQL,
743 params![workflow_id.to_string()],
744 )
745 .await
746 .map_err(|error| crate::error::libsql_error(&error))?;
747 let mut settled = Vec::new();
748 while let Some(row) = rows
749 .next()
750 .await
751 .map_err(|error| crate::error::libsql_error(&error))?
752 {
753 let dispatch_key: String = row
754 .get(0)
755 .map_err(|error| crate::error::libsql_error(&error))?;
756 settled.push(dispatch_key);
757 }
758 if !settled.is_empty() {
759 tx.execute(
760 SETTLE_WORKFLOW_LIVE_ROWS_SQL,
761 params![workflow_id.to_string()],
762 )
763 .await
764 .map_err(|error| crate::error::libsql_error(&error))?;
765 }
766 Ok(settled)
767}
768
769#[derive(Clone, Debug, PartialEq, Eq)]
774pub struct OutboxRowState {
775 pub status: OutboxStatus,
777 pub attempt: u32,
779 pub visible_after: DateTime<Utc>,
781}
782
783const SELECT_ROW_STATE_SQL: &str = "
784SELECT status, attempt, visible_after FROM outbox WHERE dispatch_key = ?1";
785
786pub(crate) async fn outbox_row_state(
788 conn: &Connection,
789 dispatch_key: &str,
790) -> Result<Option<OutboxRowState>, StoreError> {
791 let mut rows = conn
792 .query(SELECT_ROW_STATE_SQL, params![dispatch_key.to_string()])
793 .await
794 .map_err(|error| crate::error::libsql_error(&error))?;
795 let Some(row) = rows
796 .next()
797 .await
798 .map_err(|error| crate::error::libsql_error(&error))?
799 else {
800 return Ok(None);
801 };
802 let status: String = row
803 .get(0)
804 .map_err(|error| crate::error::libsql_error(&error))?;
805 let attempt: i64 = row
806 .get(1)
807 .map_err(|error| crate::error::libsql_error(&error))?;
808 let visible_after: String = row
809 .get(2)
810 .map_err(|error| crate::error::libsql_error(&error))?;
811 Ok(Some(OutboxRowState {
812 status: OutboxStatus::parse_token(&status)?,
813 attempt: u32::try_from(attempt)
814 .map_err(|_| StoreError::Backend(format!("outbox attempt out of range: {attempt}")))?,
815 visible_after: decode_instant(&visible_after)?,
816 }))
817}
818
819pub(crate) async fn count_inflight_outbox_rows(
830 conn: &Connection,
831 namespace: &str,
832) -> Result<u64, StoreError> {
833 let mut rows = conn
834 .query(COUNT_INFLIGHT_OUTBOX_SQL, params![namespace.to_string()])
835 .await
836 .map_err(|error| crate::error::libsql_error(&error))?;
837 let Some(row) = rows
838 .next()
839 .await
840 .map_err(|error| crate::error::libsql_error(&error))?
841 else {
842 return Ok(0);
843 };
844 let count: i64 = row
845 .get(0)
846 .map_err(|error| crate::error::libsql_error(&error))?;
847 u64::try_from(count)
848 .map_err(|_| StoreError::Serialization(format!("outbox in-flight count negative: {count}")))
849}
850
851pub(crate) async fn count_claimed_outbox_rows(
862 conn: &Connection,
863 namespace: &str,
864) -> Result<u64, StoreError> {
865 let mut rows = conn
866 .query(COUNT_CLAIMED_OUTBOX_SQL, params![namespace.to_string()])
867 .await
868 .map_err(|error| crate::error::libsql_error(&error))?;
869 let Some(row) = rows
870 .next()
871 .await
872 .map_err(|error| crate::error::libsql_error(&error))?
873 else {
874 return Ok(0);
875 };
876 let count: i64 = row
877 .get(0)
878 .map_err(|error| crate::error::libsql_error(&error))?;
879 u64::try_from(count)
880 .map_err(|_| StoreError::Serialization(format!("outbox claimed count negative: {count}")))
881}
882
883pub(crate) async fn count_claimed_outbox_rows_by_namespace(
895 conn: &Connection,
896 namespaces: &[&str],
897) -> Result<std::collections::BTreeMap<String, u64>, StoreError> {
898 let requested: std::collections::BTreeSet<&str> = namespaces.iter().copied().collect();
899 let mut counts: std::collections::BTreeMap<String, u64> =
900 requested.iter().map(|ns| ((*ns).to_owned(), 0)).collect();
901 let mut rows = conn
902 .query(COUNT_CLAIMED_OUTBOX_BY_NAMESPACE_SQL, ())
903 .await
904 .map_err(|error| crate::error::libsql_error(&error))?;
905 while let Some(row) = rows
906 .next()
907 .await
908 .map_err(|error| crate::error::libsql_error(&error))?
909 {
910 let namespace: String = row
911 .get(0)
912 .map_err(|error| crate::error::libsql_error(&error))?;
913 if !requested.contains(namespace.as_str()) {
914 continue;
915 }
916 let count: i64 = row
917 .get(1)
918 .map_err(|error| crate::error::libsql_error(&error))?;
919 let count = u64::try_from(count).map_err(|_| {
920 StoreError::Serialization(format!("outbox claimed count negative: {count}"))
921 })?;
922 counts.insert(namespace, count);
923 }
924 Ok(counts)
925}
926
927pub(crate) async fn pending_outbox_routes(
938 conn: &Connection,
939) -> Result<Vec<ClaimScope>, StoreError> {
940 let now = encode_instant(Utc::now());
941 let mut rows = conn
942 .query(PENDING_OUTBOX_ROUTES_SQL, params![now])
943 .await
944 .map_err(|error| crate::error::libsql_error(&error))?;
945 let mut routes = Vec::new();
946 while let Some(row) = rows
947 .next()
948 .await
949 .map_err(|error| crate::error::libsql_error(&error))?
950 {
951 let namespace: Option<String> = row
954 .get(0)
955 .map_err(|error| crate::error::libsql_error(&error))?;
956 let task_queue: Option<String> = row
957 .get(1)
958 .map_err(|error| crate::error::libsql_error(&error))?;
959 let node: Option<String> = row
960 .get(2)
961 .map_err(|error| crate::error::libsql_error(&error))?;
962 routes.push(
963 ClaimScope::new(
964 namespace.unwrap_or_else(|| String::from(DEFAULT_OUTBOX_ROUTE)),
965 task_queue.unwrap_or_else(|| String::from(DEFAULT_OUTBOX_ROUTE)),
966 )
967 .with_node(node),
968 );
969 }
970 Ok(routes)
971}
972
973fn decode_row(row: &Row) -> Result<OutboxRow, StoreError> {
974 let dispatch_key: String = row
975 .get(0)
976 .map_err(|error| crate::error::libsql_error(&error))?;
977 let workflow_id: String = row
978 .get(1)
979 .map_err(|error| crate::error::libsql_error(&error))?;
980 let ordinal: i64 = row
981 .get(2)
982 .map_err(|error| crate::error::libsql_error(&error))?;
983 let activity_type: String = row
984 .get(3)
985 .map_err(|error| crate::error::libsql_error(&error))?;
986 let input: Vec<u8> = row
987 .get(4)
988 .map_err(|error| crate::error::libsql_error(&error))?;
989 let status: String = row
990 .get(5)
991 .map_err(|error| crate::error::libsql_error(&error))?;
992 let attempt: i64 = row
993 .get(6)
994 .map_err(|error| crate::error::libsql_error(&error))?;
995 let visible_after: String = row
996 .get(7)
997 .map_err(|error| crate::error::libsql_error(&error))?;
998 let claimed_at: Option<String> = row
999 .get(8)
1000 .map_err(|error| crate::error::libsql_error(&error))?;
1001 let run_id: Option<String> = row
1002 .get(9)
1003 .map_err(|error| crate::error::libsql_error(&error))?;
1004 let namespace: Option<String> = row
1007 .get(10)
1008 .map_err(|error| crate::error::libsql_error(&error))?;
1009 let task_queue: Option<String> = row
1010 .get(11)
1011 .map_err(|error| crate::error::libsql_error(&error))?;
1012 let node: Option<String> = row
1015 .get(12)
1016 .map_err(|error| crate::error::libsql_error(&error))?;
1017
1018 Ok(OutboxRow {
1019 dispatch_key,
1020 workflow_id: decode_workflow_id(&workflow_id)?,
1021 ordinal: u64::try_from(ordinal)
1022 .map_err(|_| StoreError::Backend(format!("outbox ordinal was negative: {ordinal}")))?,
1023 activity_type,
1024 input: decode_payload(&input)?,
1025 status: OutboxStatus::parse_token(&status)?,
1026 attempt: u32::try_from(attempt)
1027 .map_err(|_| StoreError::Backend(format!("outbox attempt out of range: {attempt}")))?,
1028 visible_after: decode_instant(&visible_after)?,
1029 claimed_at: claimed_at.as_deref().map(decode_instant).transpose()?,
1030 run_id: run_id.as_deref().map(decode_run_id).transpose()?,
1031 namespace: namespace.unwrap_or_else(|| String::from(DEFAULT_OUTBOX_ROUTE)),
1032 task_queue: task_queue.unwrap_or_else(|| String::from(DEFAULT_OUTBOX_ROUTE)),
1033 node,
1034 })
1035}
1036
1037fn encode_payload(payload: &Payload) -> Result<Vec<u8>, StoreError> {
1038 serde_json::to_vec(payload).map_err(|error| crate::error::serde_json_error(&error))
1039}
1040
1041fn decode_payload(bytes: &[u8]) -> Result<Payload, StoreError> {
1042 serde_json::from_slice(bytes).map_err(|error| crate::error::serde_json_error(&error))
1043}
1044
1045fn decode_workflow_id(value: &str) -> Result<WorkflowId, StoreError> {
1046 uuid::Uuid::parse_str(value)
1047 .map(WorkflowId::new)
1048 .map_err(|error| StoreError::Serialization(format!("invalid outbox workflow id: {error}")))
1049}
1050
1051fn decode_run_id(value: &str) -> Result<RunId, StoreError> {
1052 uuid::Uuid::parse_str(value)
1053 .map(RunId::new)
1054 .map_err(|error| StoreError::Serialization(format!("invalid outbox run id: {error}")))
1055}
1056
1057fn encode_instant(instant: DateTime<Utc>) -> String {
1058 instant.to_rfc3339_opts(SecondsFormat::Nanos, true)
1059}
1060
1061fn decode_instant(value: &str) -> Result<DateTime<Utc>, StoreError> {
1062 DateTime::parse_from_rfc3339(value)
1063 .map(|date_time| date_time.with_timezone(&Utc))
1064 .map_err(|error| StoreError::Serialization(error.to_string()))
1065}
1066
1067async fn rollback(tx: Transaction) -> Result<(), StoreError> {
1068 tx.rollback()
1069 .await
1070 .map_err(|error| crate::error::libsql_error(&error))
1071}