1use crate::{histograms::Histograms, sqlite_dao::conversions::to_generic_error};
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4use concepts::{
5 ComponentId, ComponentRetryConfig, ExecutionId, FunctionFqn, JoinSetId, StrVariant,
6 SupportedFunctionReturnValue,
7 component_id::ComponentDigest,
8 prefixed_ulid::{DelayId, DeploymentId, ExecutionIdDerived, ExecutorId, RunId},
9 storage::{
10 AppendBatchResponse, AppendDelayResponseOutcome, AppendEventsToExecution, AppendRequest,
11 AppendResponse, AppendResponseToExecution, BacktraceFilter, BacktraceInfo, CreateRequest,
12 DUMMY_CREATED, DUMMY_HISTORY_EVENT, DbConnection, DbErrorGeneric, DbErrorRead,
13 DbErrorReadWithTimeout, DbErrorWrite, DbErrorWriteNonRetriable, DbExecutor, DbExternalApi,
14 DbPool, DbPoolCloseable, DeploymentRecord, DeploymentState, DeploymentStatus,
15 ExecutionEvent, ExecutionListPagination, ExecutionRequest, ExecutionWithState,
16 ExecutionWithStateRequestsResponses, ExpiredDelay, ExpiredLock, ExpiredTimer,
17 HISTORY_EVENT_TYPE_JOIN_NEXT, HistoryEvent, JoinSetRequest, JoinSetResponse,
18 JoinSetResponseEvent, JoinSetResponseEventOuter, ListExecutionEventsResponse,
19 ListExecutionsFilter, ListLogsResponse, ListResponsesResponse, LockPendingResponse, Locked,
20 LockedBy, LockedExecution, LogEntry, LogEntryRow, LogFilter, LogInfoAppendRow, LogLevel,
21 LogStreamType, Pagination, PendingState, PendingStateBlockedByJoinSet,
22 PendingStateFinishedResultKind, PendingStateMergedPause, ResponseCursor,
23 ResponseWithCursor, STATE_BLOCKED_BY_JOIN_SET, STATE_FINISHED, STATE_LOCKED,
24 STATE_PENDING_AT, TimeoutOutcome, Version, VersionType,
25 },
26};
27use const_format::formatcp;
28use conversions::{JsonWrapper, consistency_db_err, consistency_rusqlite, from_generic_error};
29use db_common::{
30 AppendNotifier, CombinedState, CombinedStateDTO, NotifierExecutionFinished, NotifierPendingAt,
31 PendingFfqnSubscribersHolder,
32};
33use hashbrown::HashMap;
34use rusqlite::{
35 CachedStatement, Connection, OpenFlags, OptionalExtension, Params, Row, ToSql, Transaction,
36 TransactionBehavior, named_params, types::ToSqlOutput,
37};
38use sha2::{Digest as _, Sha256};
39use std::{
40 cmp::max,
41 collections::VecDeque,
42 fmt::Debug,
43 ops::DerefMut,
44 panic::Location,
45 path::Path,
46 sync::{
47 Arc, Mutex,
48 atomic::{AtomicBool, Ordering},
49 },
50 time::{Duration, Instant},
51};
52use std::{fmt::Write as _, pin::Pin};
53use strum::IntoEnumIterator as _;
54use tokio::sync::{mpsc, oneshot};
55use tracing::{Level, Span, debug, error, info, instrument, trace, warn};
56use tracing_error::SpanTrace;
57
58#[derive(Debug, thiserror::Error)]
59#[error("initialization error")]
60pub struct InitializationError;
61
62#[derive(Debug, Clone)]
63struct DelayReq {
64 join_set_id: JoinSetId,
65 delay_id: DelayId,
66 expires_at: DateTime<Utc>,
67}
68const PRAGMA: [[&str; 2]; 10] = [
80 ["journal_mode", "wal"],
81 ["synchronous", "FULL"],
82 ["foreign_keys", "true"],
83 ["busy_timeout", "1000"],
84 ["cache_size", "10000"], ["temp_store", "MEMORY"],
86 ["page_size", "8192"], ["mmap_size", "134217728"],
88 ["journal_size_limit", "67108864"],
89 ["integrity_check", ""],
90];
91
92const CREATE_TABLE_T_METADATA: &str = r"
94CREATE TABLE IF NOT EXISTS t_metadata (
95 id INTEGER PRIMARY KEY AUTOINCREMENT,
96 schema_version INTEGER NOT NULL,
97 created_at TEXT NOT NULL
98) STRICT
99";
100const T_METADATA_EXPECTED_SCHEMA_VERSION: u32 = 7;
101
102const CREATE_TABLE_T_EXECUTION_LOG: &str = r"
104CREATE TABLE IF NOT EXISTS t_execution_log (
105 execution_id TEXT NOT NULL,
106 created_at TEXT NOT NULL,
107 json_value TEXT NOT NULL,
108 version INTEGER NOT NULL,
109 variant TEXT NOT NULL,
110 join_set_id TEXT,
111 history_event_type TEXT GENERATED ALWAYS AS (json_value->>'$.history_event.event.type') STORED,
112 PRIMARY KEY (execution_id, version)
113) STRICT
114";
115const CREATE_INDEX_IDX_T_EXECUTION_LOG_EXECUTION_ID_VERSION: &str = r"
117CREATE INDEX IF NOT EXISTS idx_t_execution_log_execution_id_version ON t_execution_log (execution_id, version);
118";
119const CREATE_INDEX_IDX_T_EXECUTION_LOG_EXECUTION_ID_VARIANT: &str = r"
121CREATE INDEX IF NOT EXISTS idx_t_execution_log_execution_id_variant ON t_execution_log (execution_id, variant);
122";
123
124const CREATE_INDEX_IDX_T_EXECUTION_LOG_EXECUTION_ID_JOIN_SET: &str = const_format::formatcp!(
126 "CREATE INDEX IF NOT EXISTS idx_t_execution_log_execution_id_join_set ON t_execution_log (execution_id, join_set_id, history_event_type) WHERE history_event_type=\"{}\";",
127 HISTORY_EVENT_TYPE_JOIN_NEXT
128);
129
130const CREATE_TABLE_T_JOIN_SET_RESPONSE: &str = r"
135CREATE TABLE IF NOT EXISTS t_join_set_response (
136 id INTEGER PRIMARY KEY AUTOINCREMENT,
137 created_at TEXT NOT NULL,
138 execution_id TEXT NOT NULL,
139 join_set_id TEXT NOT NULL,
140
141 delay_id TEXT,
142 delay_success INTEGER,
143
144 child_execution_id TEXT,
145 finished_version INTEGER,
146
147 UNIQUE (execution_id, join_set_id, delay_id, child_execution_id)
148) STRICT
149";
150const CREATE_INDEX_IDX_T_JOIN_SET_RESPONSE_EXECUTION_ID_ID: &str = r"
152CREATE INDEX IF NOT EXISTS idx_t_join_set_response_execution_id_id ON t_join_set_response (execution_id, id);
153";
154const CREATE_INDEX_IDX_JOIN_SET_RESPONSE_UNIQUE_CHILD_ID: &str = r"
156CREATE UNIQUE INDEX IF NOT EXISTS idx_join_set_response_unique_child_id
157ON t_join_set_response (child_execution_id) WHERE child_execution_id IS NOT NULL;
158";
159const CREATE_INDEX_IDX_JOIN_SET_RESPONSE_UNIQUE_DELAY_ID: &str = r"
161CREATE UNIQUE INDEX IF NOT EXISTS idx_join_set_response_unique_delay_id
162ON t_join_set_response (delay_id) WHERE delay_id IS NOT NULL;
163";
164
165const CREATE_TABLE_T_STATE: &str = r"
192CREATE TABLE IF NOT EXISTS t_state (
193 execution_id TEXT NOT NULL,
194 is_top_level INTEGER NOT NULL,
195 corresponding_version INTEGER NOT NULL,
196 ffqn TEXT NOT NULL,
197 created_at TEXT NOT NULL,
198 component_id_input_digest BLOB NOT NULL,
199 component_type TEXT NOT NULL,
200 first_scheduled_at TEXT NOT NULL,
201 deployment_id TEXT NOT NULL,
202 is_paused INTEGER NOT NULL,
203
204 pending_expires_finished TEXT NOT NULL,
205 state TEXT NOT NULL,
206 updated_at TEXT NOT NULL,
207 intermittent_event_count INTEGER NOT NULL,
208
209 max_retries INTEGER,
210 retry_exp_backoff_millis INTEGER,
211 last_lock_version INTEGER,
212 executor_id TEXT,
213 run_id TEXT,
214
215 join_set_id TEXT,
216 join_set_closing INTEGER,
217
218 result_kind TEXT,
219
220 PRIMARY KEY (execution_id)
221) STRICT
222";
223
224const IDX_T_STATE_LOCK_PENDING_BY_FFQN: &str = formatcp!(
226 r"
227CREATE INDEX IF NOT EXISTS idx_t_state_lock_pending_by_ffqn ON t_state (pending_expires_finished, ffqn) WHERE state = '{}';
228",
229 STATE_PENDING_AT
230);
231const IDX_T_STATE_LOCK_PENDING_BY_COMPONENT: &str = formatcp!(
233 r"
234CREATE INDEX IF NOT EXISTS idx_t_state_lock_pending_by_component ON t_state (pending_expires_finished, component_id_input_digest) WHERE state = '{}';
235",
236 STATE_PENDING_AT
237);
238const IDX_T_STATE_EXPIRED_LOCKS: &str = formatcp!(
239 "CREATE INDEX IF NOT EXISTS idx_t_state_expired_locks ON t_state (pending_expires_finished) WHERE state = '{}';",
240 STATE_LOCKED
241);
242const IDX_T_STATE_EXECUTION_ID_IS_TOP_LEVEL: &str = r"
243CREATE INDEX IF NOT EXISTS idx_t_state_execution_id_is_root ON t_state (execution_id, is_top_level);
244";
245const IDX_T_STATE_FFQN: &str = r"
247CREATE INDEX IF NOT EXISTS idx_t_state_ffqn ON t_state (ffqn);
248";
249const IDX_T_STATE_CREATED_AT: &str = r"
251CREATE INDEX IF NOT EXISTS idx_t_state_created_at ON t_state (created_at);
252";
253
254const IDX_T_STATE_DEPLOYMENT_STATE: &str = r"
256CREATE INDEX IF NOT EXISTS idx_t_state_deployment_state ON t_state (deployment_id, state);
257";
258
259const CREATE_TABLE_T_DELAY: &str = r"
261CREATE TABLE IF NOT EXISTS t_delay (
262 execution_id TEXT NOT NULL,
263 join_set_id TEXT NOT NULL,
264 delay_id TEXT NOT NULL,
265 expires_at TEXT NOT NULL,
266 PRIMARY KEY (execution_id, join_set_id, delay_id)
267) STRICT
268";
269
270const CREATE_TABLE_T_EXECUTION_BACKTRACE: &str = r"
273CREATE TABLE IF NOT EXISTS t_execution_backtrace (
274 execution_id TEXT NOT NULL,
275 component_id TEXT NOT NULL,
276 version_min_including INTEGER NOT NULL,
277 version_max_excluding INTEGER NOT NULL,
278 backtrace_hash BLOB NOT NULL,
279
280 PRIMARY KEY (
281 execution_id,
282 version_min_including,
283 version_max_excluding
284 ),
285 FOREIGN KEY (backtrace_hash)
286 REFERENCES t_wasm_backtrace(backtrace_hash)
287) STRICT
288";
289const IDX_T_EXECUTION_BACKTRACE_EXECUTION_ID_VERSION: &str = r"
291CREATE INDEX IF NOT EXISTS idx_t_execution_backtrace_execution_id_version
292ON t_execution_backtrace (
293 execution_id,
294 version_min_including,
295 version_max_excluding
296)
297";
298const CREATE_TABLE_T_WASM_BACKTRACE: &str = r"
300CREATE TABLE IF NOT EXISTS t_wasm_backtrace (
301 backtrace_hash BLOB NOT NULL,
302 wasm_backtrace TEXT NOT NULL,
303
304 PRIMARY KEY (backtrace_hash)
305) STRICT
306";
307
308const CREATE_TABLE_T_SOURCE_FILE: &str = r"
310CREATE TABLE IF NOT EXISTS t_source_file (
311 content_hash BLOB NOT NULL,
312 content TEXT NOT NULL,
313
314 PRIMARY KEY (content_hash)
315) STRICT
316";
317const CREATE_TABLE_T_COMPONENT_SOURCE: &str = r"
320CREATE TABLE IF NOT EXISTS t_component_source (
321 component_digest BLOB NOT NULL,
322 frame_key TEXT NOT NULL,
323 is_suffix INTEGER NOT NULL,
324 content_hash BLOB NOT NULL,
325
326 PRIMARY KEY (component_digest, frame_key, is_suffix),
327 FOREIGN KEY (content_hash)
328 REFERENCES t_source_file(content_hash)
329) STRICT
330";
331
332const CREATE_TABLE_T_LOG: &str = r"
336CREATE TABLE IF NOT EXISTS t_log (
337 id INTEGER PRIMARY KEY,
338 execution_id TEXT NOT NULL,
339 run_id TEXT NOT NULL,
340 created_at TEXT NOT NULL,
341 level INTEGER,
342 message TEXT,
343 stream_type INTEGER,
344 payload BLOB
345) STRICT
346";
347const IDX_T_LOG_EXECUTION_ID_RUN_ID_CREATED_AT: &str = r"
348CREATE INDEX IF NOT EXISTS idx_t_log_execution_id_run_id_created_at ON t_log (execution_id, run_id, created_at);
349";
350const IDX_T_LOG_EXECUTION_ID_CREATED_AT: &str = r"
351CREATE INDEX IF NOT EXISTS idx_t_log_execution_id_created_at ON t_log (execution_id, created_at);
352";
353
354const CREATE_TABLE_T_DEPLOYMENT: &str = r"
355CREATE TABLE IF NOT EXISTS t_deployment (
356 deployment_id TEXT NOT NULL PRIMARY KEY,
357 created_at TEXT NOT NULL,
358 last_active_at TEXT,
359 status TEXT NOT NULL,
360 config_json TEXT NOT NULL,
361 obelisk_version TEXT NOT NULL,
362 created_by TEXT
363) STRICT
364";
365const IDX_T_DEPLOYMENT_STATUS: &str = r"
366CREATE INDEX IF NOT EXISTS idx_t_deployment_status ON t_deployment (status)
367";
368const IDX_T_DEPLOYMENT_SINGLE_ACTIVE: &str = r"
370CREATE UNIQUE INDEX IF NOT EXISTS idx_t_deployment_single_active ON t_deployment ((1)) WHERE status = 'active'
371";
372const IDX_T_DEPLOYMENT_SINGLE_ENQUEUED: &str = r"
374CREATE UNIQUE INDEX IF NOT EXISTS idx_t_deployment_single_enqueued ON t_deployment ((1)) WHERE status = 'enqueued'
375";
376
377#[derive(Debug, thiserror::Error, Clone)]
378enum RusqliteError {
379 #[error("not found")]
380 NotFound,
381 #[error("generic: {reason}")]
382 Generic {
383 reason: StrVariant,
384 context: SpanTrace,
385 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
386 loc: &'static Location<'static>,
387 },
388 #[error("close")]
389 Close,
390}
391
392mod conversions {
393
394 use super::RusqliteError;
395 use concepts::{
396 StrVariant,
397 storage::{DbErrorGeneric, DbErrorRead, DbErrorReadWithTimeout, DbErrorWrite},
398 };
399 use rusqlite::{
400 ToSql,
401 types::{FromSql, FromSqlError},
402 };
403 use std::{fmt::Debug, panic::Location, sync::Arc};
404 use tracing::error;
405 use tracing_error::SpanTrace;
406
407 impl From<rusqlite::Error> for RusqliteError {
408 #[track_caller]
410 fn from(err: rusqlite::Error) -> Self {
411 if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
412 RusqliteError::NotFound
413 } else {
414 RusqliteError::Generic {
415 reason: err.to_string().into(),
416 context: SpanTrace::capture(),
417 source: Some(Arc::new(err)),
418 loc: Location::caller(),
419 }
420 }
421 }
422 }
423
424 #[track_caller]
426 pub fn to_generic_error(err: RusqliteError) -> DbErrorGeneric {
427 if let RusqliteError::Close = err {
428 DbErrorGeneric::Close
429 } else {
430 DbErrorGeneric::Uncategorized {
431 reason: err.to_string().into(),
432 context: SpanTrace::capture(),
433 source: Some(Arc::new(err)),
434 loc: Location::caller(),
435 }
436 }
437 }
438
439 #[track_caller]
441 pub fn from_generic_error(err: &DbErrorGeneric) -> rusqlite::Error {
442 FromSqlError::other(OtherError {
443 reason: err.to_string().into(),
444 loc: Location::caller(),
445 })
446 .into()
447 }
448
449 impl From<RusqliteError> for DbErrorRead {
450 fn from(err: RusqliteError) -> Self {
451 if matches!(err, RusqliteError::NotFound) {
452 Self::NotFound
453 } else {
454 to_generic_error(err).into()
455 }
456 }
457 }
458 impl From<RusqliteError> for DbErrorReadWithTimeout {
459 fn from(err: RusqliteError) -> Self {
460 Self::from(DbErrorRead::from(err))
461 }
462 }
463 impl From<RusqliteError> for DbErrorWrite {
464 fn from(err: RusqliteError) -> Self {
465 if matches!(err, RusqliteError::NotFound) {
466 Self::NotFound
467 } else {
468 to_generic_error(err).into()
469 }
470 }
471 }
472
473 pub(crate) struct JsonWrapper<T>(pub(crate) T);
474 impl<T: serde::de::DeserializeOwned + 'static + Debug> FromSql for JsonWrapper<T> {
475 fn column_result(
476 value: rusqlite::types::ValueRef<'_>,
477 ) -> rusqlite::types::FromSqlResult<Self> {
478 let value = match value {
479 rusqlite::types::ValueRef::Text(value) | rusqlite::types::ValueRef::Blob(value) => {
480 Ok(value)
481 }
482 other => {
483 error!(
484 backtrace = %std::backtrace::Backtrace::capture(),
485 "Unexpected type when conveting to JSON - expected Text or Blob, got type `{other:?}`",
486 );
487 Err(FromSqlError::InvalidType)
488 }
489 }?;
490 let value = serde_json::from_slice::<T>(value).map_err(|err| {
491 error!(
492 backtrace = %std::backtrace::Backtrace::capture(),
493 "Cannot convert JSON value `{value:?}` to type:`{type}` - {err:?}",
494 r#type = std::any::type_name::<T>()
495 );
496 FromSqlError::InvalidType
497 })?;
498 Ok(Self(value))
499 }
500 }
501 impl<T: serde::ser::Serialize + Debug> ToSql for JsonWrapper<T> {
502 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
503 let string = serde_json::to_string(&self.0).map_err(|err| {
504 error!(
505 "Cannot serialize {value:?} of type `{type}` - {err:?}",
506 value = self.0,
507 r#type = std::any::type_name::<T>()
508 );
509 rusqlite::Error::ToSqlConversionFailure(Box::new(err))
510 })?;
511 Ok(rusqlite::types::ToSqlOutput::Owned(
512 rusqlite::types::Value::Text(string),
513 ))
514 }
515 }
516
517 #[derive(Debug, thiserror::Error)]
519 #[error("{reason}")]
520 pub(crate) struct OtherError {
521 reason: StrVariant,
522 loc: &'static Location<'static>,
523 }
524
525 #[track_caller]
526 pub(crate) fn consistency_rusqlite(reason: impl Into<StrVariant>) -> rusqlite::Error {
527 FromSqlError::other(OtherError {
528 reason: reason.into(),
529 loc: Location::caller(),
530 })
531 .into()
532 }
533
534 #[track_caller]
535 pub(crate) fn consistency_db_err(reason: impl Into<StrVariant>) -> DbErrorGeneric {
536 DbErrorGeneric::Uncategorized {
537 reason: reason.into(),
538 context: SpanTrace::capture(),
539 source: None,
540 loc: Location::caller(),
541 }
542 }
543}
544
545#[derive(Debug, Copy, Clone, PartialEq, Eq)]
546enum TxType {
547 MultipleWrites, Other, }
550
551#[derive(Clone)]
552struct CommitError(RusqliteError);
553
554#[derive(Debug)]
555struct ShouldRollback;
556
557#[derive(derive_more::Debug)]
558struct LogicalTx {
559 #[debug(skip)]
560 #[expect(clippy::type_complexity)]
561 func: Box<dyn FnMut(&mut Transaction) -> Result<(), ShouldRollback> + Send>,
562 sent_at: Instant,
563 func_name: &'static str,
564 #[debug(skip)]
565 phytx_flush_sender: oneshot::Sender<Result<(), CommitError>>,
567 priority: LtxPriority,
568}
569#[derive(Copy, Clone, Debug, PartialEq, Eq)]
570enum LtxPriority {
571 High,
572 Low,
573}
574
575#[derive(derive_more::Debug)]
576enum ThreadCommand {
577 LogicalTx(LogicalTx),
578 Shutdown,
579}
580
581#[derive(Clone)]
582pub struct SqlitePool(SqlitePoolInner);
583
584type ResponseSubscribers =
585 Arc<Mutex<HashMap<ExecutionId, (oneshot::Sender<ResponseWithCursor>, u64)>>>;
586type PendingSubscribers = Arc<Mutex<PendingFfqnSubscribersHolder>>;
587type ExecutionFinishedSubscribers =
588 Mutex<HashMap<ExecutionId, HashMap<u64, oneshot::Sender<SupportedFunctionReturnValue>>>>;
589
590#[derive(Clone)]
591struct SqlitePoolInner {
592 shutdown_requested: Arc<AtomicBool>,
593 shutdown_finished: Arc<AtomicBool>,
594 command_tx: tokio::sync::mpsc::Sender<ThreadCommand>,
595 response_subscribers: ResponseSubscribers,
596 pending_subscribers: PendingSubscribers,
597 execution_finished_subscribers: Arc<ExecutionFinishedSubscribers>,
598 join_handle: Option<Arc<std::thread::JoinHandle<()>>>, }
600
601#[async_trait]
602impl DbPoolCloseable for SqlitePool {
603 async fn close(&self) {
604 debug!("Sqlite is closing");
605 self.0.shutdown_requested.store(true, Ordering::Release);
606 let _ = self.0.command_tx.try_send(ThreadCommand::Shutdown);
608 while !self.0.shutdown_finished.load(Ordering::Acquire) {
609 tokio::time::sleep(Duration::from_millis(1)).await;
610 }
611 debug!("Sqlite was closed");
612 }
613}
614
615#[async_trait]
616impl DbPool for SqlitePool {
617 async fn db_exec_conn(&self) -> Result<Box<dyn DbExecutor>, DbErrorGeneric> {
618 if self.0.shutdown_requested.load(Ordering::Acquire) {
619 return Err(DbErrorGeneric::Close);
620 }
621 Ok(Box::new(self.clone()))
622 }
623
624 async fn connection(&self) -> Result<Box<dyn DbConnection>, DbErrorGeneric> {
625 if self.0.shutdown_requested.load(Ordering::Acquire) {
626 return Err(DbErrorGeneric::Close);
627 }
628 Ok(Box::new(self.clone()))
629 }
630 async fn external_api_conn(&self) -> Result<Box<dyn DbExternalApi>, DbErrorGeneric> {
631 if self.0.shutdown_requested.load(Ordering::Acquire) {
632 return Err(DbErrorGeneric::Close);
633 }
634 Ok(Box::new(self.clone()))
635 }
636 #[cfg(feature = "test")]
637 async fn connection_test(
638 &self,
639 ) -> Result<Box<dyn concepts::storage::DbConnectionTest>, DbErrorGeneric> {
640 if self.0.shutdown_requested.load(Ordering::Acquire) {
641 return Err(DbErrorGeneric::Close);
642 }
643 Ok(Box::new(self.clone()))
644 }
645}
646
647impl Drop for SqlitePool {
648 fn drop(&mut self) {
649 let arc = self.0.join_handle.take().expect("join_handle was set");
650 if let Ok(join_handle) = Arc::try_unwrap(arc) {
651 if !join_handle.is_finished() {
653 if !self.0.shutdown_finished.load(Ordering::Acquire) {
654 let backtrace = std::backtrace::Backtrace::capture();
656 warn!("SqlitePool was not closed properly - {backtrace}");
657 self.0.shutdown_requested.store(true, Ordering::Release);
658 let _ = self.0.command_tx.try_send(ThreadCommand::Shutdown);
660 } else {
663 }
665 }
666 }
667 }
668}
669
670#[derive(Debug, Clone)]
671pub struct SqliteConfig {
672 pub queue_capacity: usize,
673 pub pragma_override: Option<HashMap<String, String>>,
674 pub metrics_threshold: Option<Duration>,
675}
676impl Default for SqliteConfig {
677 fn default() -> Self {
678 Self {
679 queue_capacity: 100,
680 pragma_override: None,
681 metrics_threshold: None,
682 }
683 }
684}
685
686struct ShutdownRequested;
687
688fn deployment_record_from_row(row: &Row<'_>) -> rusqlite::Result<DeploymentRecord> {
689 let deployment_id: DeploymentId = row.get("deployment_id")?;
690 let status_str: String = row.get("status")?;
691 let status = status_str.parse::<DeploymentStatus>().map_err(|_| {
692 rusqlite::Error::InvalidColumnType(3, "status".to_string(), rusqlite::types::Type::Text)
693 })?;
694 Ok(DeploymentRecord {
695 deployment_id,
696 created_at: row.get("created_at")?,
697 last_active_at: row.get("last_active_at")?,
698 status,
699 config_json: row.get("config_json")?,
700 obelisk_version: row.get("obelisk_version")?,
701 created_by: row.get("created_by")?,
702 })
703}
704
705impl SqlitePool {
706 fn init_thread(
707 path: &Path,
708 mut pragma_override: HashMap<String, String>,
709 ) -> Result<Connection, InitializationError> {
710 fn conn_execute<P: Params>(
711 conn: &Connection,
712 sql: &str,
713 params: P,
714 ) -> Result<(), InitializationError> {
715 conn.execute(sql, params).map(|_| ()).map_err(|err| {
716 error!("Cannot run `{sql}` - {err:?}");
717 InitializationError
718 })
719 }
720 fn pragma_update(
721 conn: &Connection,
722 name: &str,
723 value: &str,
724 ) -> Result<(), InitializationError> {
725 if value.is_empty() {
726 debug!("Querying PRAGMA {name}");
727 conn.pragma_query(None, name, |row| {
728 debug!("{row:?}");
729 Ok(())
730 })
731 .map_err(|err| {
732 error!("cannot update pragma `{name}`=`{value}` - {err:?}");
733 InitializationError
734 })
735 } else {
736 debug!("Setting PRAGMA {name}={value}");
737 conn.pragma_update(None, name, value).map_err(|err| {
738 error!("cannot update pragma `{name}`=`{value}` - {err:?}");
739 InitializationError
740 })
741 }
742 }
743
744 let conn = Connection::open_with_flags(path, OpenFlags::default()).map_err(|err| {
745 error!("cannot open the connection - {err:?}");
746 InitializationError
747 })?;
748
749 for [pragma_name, default_value] in PRAGMA {
750 let pragma_value = pragma_override
751 .remove(pragma_name)
752 .unwrap_or_else(|| default_value.to_string());
753 pragma_update(&conn, pragma_name, &pragma_value)?;
754 }
755 for (pragma_name, pragma_value) in pragma_override.drain() {
757 pragma_update(&conn, &pragma_name, &pragma_value)?;
758 }
759
760 {
762 conn_execute(&conn, CREATE_TABLE_T_METADATA, [])?;
763 let actual_version = conn
766 .prepare("SELECT schema_version FROM t_metadata ORDER BY id DESC LIMIT 1")
767 .map_err(|err| {
768 error!("cannot select schema version - {err:?}");
769 InitializationError
770 })?
771 .query_row([], |row| row.get::<_, u32>("schema_version"))
772 .optional()
773 .map_err(|err| {
774 error!("Cannot read the schema version - {err:?}");
775 InitializationError
776 })?;
777
778 match actual_version {
779 None => conn_execute(
780 &conn,
781 &format!(
782 "INSERT INTO t_metadata (schema_version, created_at) VALUES
783 ({T_METADATA_EXPECTED_SCHEMA_VERSION}, ?) ON CONFLICT DO NOTHING"
784 ),
785 [Utc::now()],
786 )?,
787 Some(actual_version) => {
788 if actual_version != T_METADATA_EXPECTED_SCHEMA_VERSION {
790 error!(
791 "wrong schema version, expected {T_METADATA_EXPECTED_SCHEMA_VERSION}, got {actual_version}"
792 );
793 return Err(InitializationError);
794 }
795 }
796 }
797 }
798
799 conn_execute(&conn, CREATE_TABLE_T_EXECUTION_LOG, [])?;
801 conn_execute(
802 &conn,
803 CREATE_INDEX_IDX_T_EXECUTION_LOG_EXECUTION_ID_VERSION,
804 [],
805 )?;
806 conn_execute(
807 &conn,
808 CREATE_INDEX_IDX_T_EXECUTION_LOG_EXECUTION_ID_VARIANT,
809 [],
810 )?;
811 conn_execute(
812 &conn,
813 CREATE_INDEX_IDX_T_EXECUTION_LOG_EXECUTION_ID_JOIN_SET,
814 [],
815 )?;
816 conn_execute(&conn, CREATE_TABLE_T_JOIN_SET_RESPONSE, [])?;
818 conn_execute(
819 &conn,
820 CREATE_INDEX_IDX_T_JOIN_SET_RESPONSE_EXECUTION_ID_ID,
821 [],
822 )?;
823 conn_execute(
824 &conn,
825 CREATE_INDEX_IDX_JOIN_SET_RESPONSE_UNIQUE_CHILD_ID,
826 [],
827 )?;
828 conn_execute(
829 &conn,
830 CREATE_INDEX_IDX_JOIN_SET_RESPONSE_UNIQUE_DELAY_ID,
831 [],
832 )?;
833 conn_execute(&conn, CREATE_TABLE_T_STATE, [])?;
835 conn_execute(&conn, IDX_T_STATE_LOCK_PENDING_BY_FFQN, [])?;
836 conn_execute(&conn, IDX_T_STATE_LOCK_PENDING_BY_COMPONENT, [])?;
837 conn_execute(&conn, IDX_T_STATE_EXPIRED_LOCKS, [])?;
838 conn_execute(&conn, IDX_T_STATE_EXECUTION_ID_IS_TOP_LEVEL, [])?;
839 conn_execute(&conn, IDX_T_STATE_FFQN, [])?;
840 conn_execute(&conn, IDX_T_STATE_CREATED_AT, [])?;
841 conn_execute(&conn, IDX_T_STATE_DEPLOYMENT_STATE, [])?;
842 conn_execute(&conn, CREATE_TABLE_T_DELAY, [])?;
844 conn_execute(&conn, CREATE_TABLE_T_EXECUTION_BACKTRACE, [])?;
846 conn_execute(&conn, IDX_T_EXECUTION_BACKTRACE_EXECUTION_ID_VERSION, [])?;
847 conn_execute(&conn, CREATE_TABLE_T_WASM_BACKTRACE, [])?;
848 conn_execute(&conn, CREATE_TABLE_T_SOURCE_FILE, [])?;
850 conn_execute(&conn, CREATE_TABLE_T_COMPONENT_SOURCE, [])?;
851 conn_execute(&conn, CREATE_TABLE_T_LOG, [])?;
853 conn_execute(&conn, IDX_T_LOG_EXECUTION_ID_RUN_ID_CREATED_AT, [])?;
854 conn_execute(&conn, IDX_T_LOG_EXECUTION_ID_CREATED_AT, [])?;
855 conn_execute(&conn, CREATE_TABLE_T_DEPLOYMENT, [])?;
857 conn_execute(&conn, IDX_T_DEPLOYMENT_STATUS, [])?;
858 conn_execute(&conn, IDX_T_DEPLOYMENT_SINGLE_ACTIVE, [])?;
859 conn_execute(&conn, IDX_T_DEPLOYMENT_SINGLE_ENQUEUED, [])?;
860 Ok(conn)
861 }
862
863 fn connection_rpc(
864 mut conn: Connection,
865 shutdown_requested: &AtomicBool,
866 shutdown_finished: &AtomicBool,
867 mut command_rx: mpsc::Receiver<ThreadCommand>,
868 metrics_threshold: Option<Duration>,
869 ) {
870 let mut histograms = Histograms::new(metrics_threshold);
871 while Self::tick(
872 &mut conn,
873 shutdown_requested,
874 &mut command_rx,
875 &mut histograms,
876 )
877 .is_ok()
878 {
879 }
881 debug!("Closing command thread");
882 shutdown_finished.store(true, Ordering::Release);
883 }
884
885 fn tick(
886 conn: &mut Connection,
887 shutdown_requested: &AtomicBool,
888 command_rx: &mut mpsc::Receiver<ThreadCommand>,
889 histograms: &mut Histograms,
890 ) -> Result<(), ShutdownRequested> {
891 #[derive(Clone, Copy, PartialEq, Eq)]
892 enum ApplyOrSkip {
893 Apply,
894 Skip, }
896 let mut ltx_list: Vec<(LogicalTx, ApplyOrSkip)> = Vec::new();
897 loop {
899 let ltx = match command_rx.blocking_recv() {
900 Some(ThreadCommand::LogicalTx(ltx)) => ltx,
901 Some(ThreadCommand::Shutdown) => {
902 debug!("Shutdown message received");
903 return Err(ShutdownRequested);
904 }
905 None => {
906 debug!("command_rx was closed");
907 return Err(ShutdownRequested);
908 }
909 };
910 let prio = ltx.priority;
911 ltx_list.push((ltx, ApplyOrSkip::Apply));
912 if prio == LtxPriority::High {
913 break;
914 }
915 }
916 let all_fns_start = std::time::Instant::now();
919
920 while let Ok(more) = command_rx.try_recv() {
922 let ltx = match more {
923 ThreadCommand::Shutdown => {
924 debug!("Shutdown message received");
925 return Err(ShutdownRequested);
928 }
929 ThreadCommand::LogicalTx(ltx) => ltx,
930 };
931 ltx_list.push((ltx, ApplyOrSkip::Apply));
932 }
933
934 struct NeedsRestart;
935 type CommitResult = Result<(), CommitError>;
936 fn try_apply_all(
937 mut ptx: Transaction<'_>,
938 ltx_list: &mut [(LogicalTx, ApplyOrSkip)],
939 histograms: &mut Histograms,
940 all_fns_start: Instant,
941 ) -> Result<CommitResult, NeedsRestart> {
942 for (ltx, former_res) in ltx_list
943 .iter_mut()
944 .filter(|(_, former_res)| *former_res == ApplyOrSkip::Apply)
945 {
946 if let Ok(()) = SqlitePool::ltx_apply_to_phytx(ltx, &mut ptx, histograms) {
947 } else {
948 *former_res = ApplyOrSkip::Skip; return Err(NeedsRestart);
951 }
952 }
953 histograms.record_all_fns(all_fns_start.elapsed());
955 let now = std::time::Instant::now();
956 let commit_result = ptx.commit().map_err(|err| {
957 warn!("Cannot commit transaction - {err:?}");
958 CommitError(RusqliteError::from(err))
959 });
960 histograms.record_commit(now.elapsed());
961 Ok(commit_result)
962 }
963
964 fn apply_all(
965 conn: &mut Connection,
966 ltx_list: &mut [(LogicalTx, ApplyOrSkip)],
967 histograms: &mut Histograms,
968 all_fns_start: Instant,
969 shutdown_requested: &AtomicBool,
970 ) -> Result<CommitResult, ShutdownRequested> {
971 loop {
973 match conn.transaction_with_behavior(TransactionBehavior::Immediate) {
974 Ok(ptx) => {
975 if let Ok(commit_res) =
976 try_apply_all(ptx, ltx_list, histograms, all_fns_start)
977 {
978 return Ok(commit_res);
979 }
980 }
981 Err(begin_err) => {
982 error!("Cannot open transaction - {begin_err:?}");
983 std::thread::sleep(Duration::from_millis(100));
984 if shutdown_requested.load(Ordering::Acquire) {
985 return Err(ShutdownRequested);
986 }
987 }
988 }
989 }
990 }
991 let ok_or_commit_error = apply_all(
992 conn,
993 &mut ltx_list,
994 histograms,
995 all_fns_start,
996 shutdown_requested,
997 )?;
998
999 for (ltx, apply_or_skip) in ltx_list {
1000 let to_send = match apply_or_skip {
1001 ApplyOrSkip::Apply => ok_or_commit_error.clone(),
1002 ApplyOrSkip::Skip => {
1003 Ok(()) }
1005 };
1006 let _ = ltx.phytx_flush_sender.send(to_send);
1008 }
1009
1010 histograms.print_if_elapsed();
1011 Ok(())
1012 }
1013
1014 fn ltx_apply_to_phytx(
1016 ltx: &mut LogicalTx,
1017 physical_tx: &mut Transaction,
1018 histograms: &mut Histograms,
1019 ) -> Result<(), ShouldRollback> {
1020 let sent_latency = ltx.sent_at.elapsed();
1021 let started_at = Instant::now();
1022 let res = (ltx.func)(physical_tx);
1023 histograms.record_command(sent_latency, ltx.func_name, started_at.elapsed());
1024 res
1025 }
1026
1027 #[instrument(skip_all, name = "sqlite_new")]
1028 pub async fn new<P: AsRef<Path>>(
1029 path: P,
1030 config: SqliteConfig,
1031 ) -> Result<Self, InitializationError> {
1032 let path = path.as_ref().to_owned();
1033
1034 let shutdown_requested = Arc::new(AtomicBool::new(false));
1035 let shutdown_finished = Arc::new(AtomicBool::new(false));
1036
1037 let (command_tx, command_rx) = tokio::sync::mpsc::channel(config.queue_capacity);
1038 info!("Sqlite database location: {path:?}");
1039 let join_handle = {
1040 let init_task = {
1042 tokio::task::spawn_blocking(move || {
1043 Self::init_thread(&path, config.pragma_override.unwrap_or_default())
1044 })
1045 .await
1046 };
1047 let conn = match init_task {
1048 Ok(res) => res?,
1049 Err(join_err) => {
1050 error!("Initialization panic - {join_err:?}");
1051 return Err(InitializationError);
1052 }
1053 };
1054 let shutdown_requested = shutdown_requested.clone();
1055 let shutdown_finished = shutdown_finished.clone();
1056 std::thread::spawn(move || {
1058 Self::connection_rpc(
1059 conn,
1060 &shutdown_requested,
1061 &shutdown_finished,
1062 command_rx,
1063 config.metrics_threshold,
1064 );
1065 })
1066 };
1067 Ok(SqlitePool(SqlitePoolInner {
1068 shutdown_requested,
1069 shutdown_finished,
1070 command_tx,
1071 response_subscribers: Arc::default(),
1072 pending_subscribers: Arc::default(),
1073 join_handle: Some(Arc::new(join_handle)),
1074 execution_finished_subscribers: Arc::default(),
1075 }))
1076 }
1077
1078 async fn transaction<F, T, E>(
1080 &self,
1081 mut func: F,
1082 tx_type: TxType,
1083 func_name: &'static str,
1084 ) -> Result<T, E>
1085 where
1086 F: FnMut(&mut rusqlite::Transaction) -> Result<T, E> + Send + 'static,
1087 T: Send + 'static,
1088 E: From<RusqliteError> + Send + 'static,
1089 {
1090 let fn_res: Arc<std::sync::Mutex<Option<_>>> = Arc::default();
1091 let (phytx_flush_sender, phytx_flush_receiver) = oneshot::channel();
1092 let current_span = Span::current();
1093 let thread_command_func = {
1094 let fn_res = fn_res.clone();
1095 ThreadCommand::LogicalTx(LogicalTx {
1096 func: Box::new(move |tx| {
1097 let _guard = current_span.enter();
1098 let func_res = func(tx);
1099 let res = if func_res.is_ok() {
1100 Ok(())
1101 } else {
1102 Err(ShouldRollback)
1103 };
1104 *fn_res.lock().unwrap() = Some(func_res);
1106 match tx_type {
1107 TxType::MultipleWrites => res,
1108 TxType::Other => Ok(()),
1109 }
1110 }),
1111 sent_at: Instant::now(),
1112 func_name,
1113 phytx_flush_sender,
1114 priority: LtxPriority::High,
1115 })
1116 };
1117 self.0
1118 .command_tx
1119 .send(thread_command_func)
1120 .await
1121 .map_err(|_send_err| RusqliteError::Close)?;
1122
1123 match phytx_flush_receiver.await {
1125 Ok(Ok(())) => {
1126 let mut guard = fn_res.lock().unwrap();
1127 std::mem::take(guard.deref_mut()).expect("ltx must have been run at least once")
1128 }
1129 Ok(Err(CommitError(rusqlite_err))) => Err(E::from(rusqlite_err)),
1130 Err(_) => Err(E::from(RusqliteError::Close)),
1131 }
1132 }
1133
1134 async fn transaction_fire_forget<F, T, E>(&self, mut func: F, func_name: &'static str)
1136 where
1137 F: FnMut(&mut rusqlite::Transaction) -> Result<T, E> + Send + 'static,
1138 T: Send + 'static + Default,
1139 E: From<RusqliteError> + Send + 'static,
1140 {
1141 let (commit_ack_sender, _commit_ack_receiver) = oneshot::channel(); let current_span = Span::current();
1143 let thread_command_func = {
1144 ThreadCommand::LogicalTx(LogicalTx {
1145 func: Box::new(move |tx| {
1146 let _guard = current_span.enter();
1147 let _ = func(tx);
1148 Ok(()) }),
1150 sent_at: Instant::now(),
1151 func_name,
1152 phytx_flush_sender: commit_ack_sender,
1153 priority: LtxPriority::Low,
1154 })
1155 };
1156 let _ = self.0.command_tx.send(thread_command_func).await; }
1158
1159 fn fetch_created_event(
1160 conn: &Connection,
1161 execution_id: &ExecutionId,
1162 ) -> Result<CreateRequest, DbErrorRead> {
1163 let mut stmt = conn.prepare(
1164 "SELECT created_at, json_value FROM t_execution_log WHERE \
1165 execution_id = :execution_id AND version = 0",
1166 )?;
1167 let (created_at, event) = stmt.query_row(
1168 named_params! {
1169 ":execution_id": execution_id.to_string(),
1170 },
1171 |row| {
1172 let created_at = row.get("created_at")?;
1173 let event = row
1174 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
1175 .map_err(|serde| {
1176 error!("cannot deserialize `Created` event: {row:?} - `{serde:?}`");
1177 consistency_rusqlite("cannot deserialize `Created` event")
1178 })?;
1179 Ok((created_at, event.0))
1180 },
1181 )?;
1182 if let ExecutionRequest::Created {
1183 ffqn,
1184 params,
1185 parent,
1186 scheduled_at,
1187 component_id,
1188 deployment_id,
1189 metadata,
1190 scheduled_by,
1191 } = event
1192 {
1193 Ok(CreateRequest {
1194 created_at,
1195 execution_id: execution_id.clone(),
1196 ffqn,
1197 params,
1198 parent,
1199 scheduled_at,
1200 component_id,
1201 deployment_id,
1202 metadata,
1203 scheduled_by,
1204 })
1205 } else {
1206 error!("Row with version=0 must be a `Created` event - {event:?}");
1207 Err(consistency_db_err("expected `Created` event").into())
1208 }
1209 }
1210
1211 fn check_expected_next_and_appending_version(
1212 expected_version: &Version,
1213 appending_version: &Version,
1214 ) -> Result<(), DbErrorWrite> {
1215 if *expected_version != *appending_version {
1216 debug!(
1217 "Version conflict - expected: {expected_version:?}, appending: {appending_version:?}"
1218 );
1219 return Err(DbErrorWrite::NonRetriable(
1220 DbErrorWriteNonRetriable::VersionConflict {
1221 expected: expected_version.clone(),
1222 requested: appending_version.clone(),
1223 },
1224 ));
1225 }
1226 Ok(())
1227 }
1228
1229 #[instrument(level = Level::DEBUG, skip_all, fields(execution_id = %req.execution_id))]
1230 fn create_inner(
1231 tx: &Transaction,
1232 req: CreateRequest,
1233 ) -> Result<(AppendResponse, AppendNotifier), DbErrorWrite> {
1234 trace!("create_inner");
1235
1236 let version = Version::default();
1237 let execution_id = req.execution_id.clone();
1238 let execution_id_str = execution_id.to_string();
1239 let ffqn = req.ffqn.clone();
1240 let created_at = req.created_at;
1241 let scheduled_at = req.scheduled_at;
1242 let component_id = req.component_id.clone();
1243 let deployment_id = req.deployment_id;
1244 let event = ExecutionRequest::from(req);
1245 let event_ser = serde_json::to_string(&event).map_err(|err| {
1246 error!("Cannot serialize {event:?} - {err:?}");
1247 DbErrorWriteNonRetriable::ValidationFailed("parameter serialization error".into())
1248 })?;
1249 tx.prepare(
1250 "INSERT INTO t_execution_log (execution_id, created_at, version, json_value, variant, join_set_id ) \
1251 VALUES (:execution_id, :created_at, :version, :json_value, :variant, :join_set_id)")
1252 ?
1253 .execute(named_params! {
1254 ":execution_id": &execution_id_str,
1255 ":created_at": created_at,
1256 ":version": version.0,
1257 ":json_value": event_ser,
1258 ":variant": event.variant(),
1259 ":join_set_id": event.join_set_id().map(std::string::ToString::to_string),
1260 })
1261 ?;
1262 let pending_at = {
1263 debug!("Creating with `Pending(`{scheduled_at:?}`)");
1264 tx.prepare(
1265 r"
1266 INSERT INTO t_state (
1267 execution_id,
1268 is_top_level,
1269 corresponding_version,
1270 pending_expires_finished,
1271 ffqn,
1272 state,
1273 created_at,
1274 component_id_input_digest,
1275 component_type,
1276 deployment_id,
1277 updated_at,
1278 first_scheduled_at,
1279 intermittent_event_count,
1280 is_paused
1281 )
1282 VALUES (
1283 :execution_id,
1284 :is_top_level,
1285 :corresponding_version,
1286 :pending_expires_finished,
1287 :ffqn,
1288 :state,
1289 :created_at,
1290 :component_id_input_digest,
1291 :component_type,
1292 :deployment_id,
1293 CURRENT_TIMESTAMP,
1294 :first_scheduled_at,
1295 0,
1296 false
1297 )
1298 ",
1299 )?
1300 .execute(named_params! {
1301 ":execution_id": execution_id.to_string(),
1302 ":is_top_level": execution_id.is_top_level(),
1303 ":corresponding_version": version.0,
1304 ":pending_expires_finished": scheduled_at,
1305 ":ffqn": ffqn.to_string(),
1306 ":state": STATE_PENDING_AT,
1307 ":created_at": created_at,
1308 ":component_id_input_digest": component_id.component_digest,
1309 ":component_type": component_id.component_type,
1310 ":deployment_id": deployment_id.to_string(),
1311 ":first_scheduled_at": scheduled_at,
1312 })?;
1313 AppendNotifier {
1314 pending_at: Some(NotifierPendingAt {
1315 scheduled_at,
1316 ffqn,
1317 component_input_digest: component_id.component_digest,
1318 }),
1319 execution_finished: None,
1320 response: None,
1321 }
1322 };
1323 let next_version = Version::new(version.0 + 1);
1324 Ok((next_version, pending_at))
1325 }
1326
1327 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %scheduled_at))]
1328 fn update_state_pending_after_response_appended(
1329 tx: &Transaction,
1330 execution_id: &ExecutionId,
1331 scheduled_at: DateTime<Utc>, component_input_digest: ComponentDigest,
1333 ) -> Result<AppendNotifier, DbErrorWrite> {
1334 debug!("Setting t_state to Pending(`{scheduled_at:?}`) after response appended");
1335 let mut stmt = tx
1336 .prepare_cached(
1337 r"
1338 UPDATE t_state
1339 SET
1340 pending_expires_finished = :pending_expires_finished,
1341 state = :state,
1342 updated_at = CURRENT_TIMESTAMP,
1343 max_retries = NULL,
1344 retry_exp_backoff_millis = NULL,
1345 last_lock_version = NULL,
1346
1347 join_set_id = NULL,
1348 join_set_closing = NULL,
1349
1350 result_kind = NULL
1351 WHERE execution_id = :execution_id
1352 ",
1353 )
1354 .map_err(|err| DbErrorGeneric::Uncategorized {
1355 reason: err.to_string().into(),
1356 context: SpanTrace::capture(),
1357 source: Some(Arc::new(err)),
1358 loc: Location::caller(),
1359 })?;
1360 let updated = stmt
1361 .execute(named_params! {
1362 ":execution_id": execution_id,
1363 ":pending_expires_finished": scheduled_at,
1364 ":state": STATE_PENDING_AT,
1365 })
1366 .map_err(|err| DbErrorGeneric::Uncategorized {
1367 reason: err.to_string().into(),
1368 context: SpanTrace::capture(),
1369 source: Some(Arc::new(err)),
1370 loc: Location::caller(),
1371 })?;
1372 if updated != 1 {
1373 return Err(DbErrorWrite::NotFound);
1374 }
1375 Ok(AppendNotifier {
1376 pending_at: Some(NotifierPendingAt {
1377 scheduled_at,
1378 ffqn: Self::fetch_created_event(tx, execution_id)?.ffqn,
1379 component_input_digest,
1380 }),
1381 execution_finished: None,
1382 response: None,
1383 })
1384 }
1385
1386 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %scheduled_at, %appending_version))]
1387 fn update_state_pending_after_event_appended(
1388 tx: &Transaction,
1389 execution_id: &ExecutionId,
1390 appending_version: &Version,
1391 scheduled_at: DateTime<Utc>, intermittent_failure: bool,
1393 component_input_digest: ComponentDigest,
1394 ) -> Result<(AppendResponse, AppendNotifier), DbErrorWrite> {
1395 debug!("Setting t_state to Pending(`{scheduled_at:?}`) after event appended");
1396 let mut stmt = tx.prepare_cached(
1397 r"
1398 UPDATE t_state
1399 SET
1400 corresponding_version = :appending_version,
1401 pending_expires_finished = :pending_expires_finished,
1402 state = :state,
1403 updated_at = CURRENT_TIMESTAMP,
1404 intermittent_event_count = intermittent_event_count + :intermittent_delta,
1405
1406 max_retries = NULL,
1407 retry_exp_backoff_millis = NULL,
1408 last_lock_version = NULL,
1409
1410 join_set_id = NULL,
1411 join_set_closing = NULL,
1412
1413 result_kind = NULL
1414 WHERE execution_id = :execution_id;
1415 ", )?;
1417 let updated = stmt
1418 .execute(named_params! {
1419 ":execution_id": execution_id.to_string(),
1420 ":appending_version": appending_version.0,
1421 ":pending_expires_finished": scheduled_at,
1422 ":state": STATE_PENDING_AT,
1423 ":intermittent_delta": i32::from(intermittent_failure) })
1425 .map_err(DbErrorWrite::from)?;
1426 if updated != 1 {
1427 return Err(DbErrorWrite::NotFound);
1428 }
1429 Ok((
1430 appending_version.increment(),
1431 AppendNotifier {
1432 pending_at: Some(NotifierPendingAt {
1433 scheduled_at,
1434 ffqn: Self::fetch_created_event(tx, execution_id)?.ffqn,
1435 component_input_digest,
1436 }),
1437 execution_finished: None,
1438 response: None,
1439 },
1440 ))
1441 }
1442
1443 #[expect(clippy::too_many_arguments)]
1444 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1445 fn update_state_locked_get_intermittent_event_count(
1446 tx: &Transaction,
1447 execution_id: &ExecutionId,
1448 deployment_id: DeploymentId,
1449 component_digest: &ComponentDigest,
1450 executor_id: ExecutorId,
1451 run_id: RunId,
1452 lock_expires_at: DateTime<Utc>,
1453 appending_version: &Version,
1454 retry_config: ComponentRetryConfig,
1455 ) -> Result<u32, DbErrorWrite> {
1456 debug!("Setting t_state to Locked(`{lock_expires_at:?}`)");
1457 let backoff_millis =
1458 i64::try_from(retry_config.retry_exp_backoff.as_millis()).map_err(|err| {
1459 DbErrorGeneric::Uncategorized {
1460 reason: "backoff too big".into(),
1461 context: SpanTrace::capture(),
1462 source: Some(Arc::new(err)),
1463 loc: Location::caller(),
1464 }
1465 })?; let execution_id_str = execution_id.to_string();
1467 let mut stmt = tx.prepare_cached(
1468 r"
1469 UPDATE t_state
1470 SET
1471 corresponding_version = :appending_version,
1472 pending_expires_finished = :pending_expires_finished,
1473 state = :state,
1474 updated_at = CURRENT_TIMESTAMP,
1475 deployment_id = :deployment_id,
1476 component_id_input_digest = :component_id_input_digest,
1477
1478 max_retries = :max_retries,
1479 retry_exp_backoff_millis = :retry_exp_backoff_millis,
1480 last_lock_version = :appending_version,
1481 executor_id = :executor_id,
1482 run_id = :run_id,
1483
1484 join_set_id = NULL,
1485 join_set_closing = NULL,
1486
1487 result_kind = NULL
1488 WHERE execution_id = :execution_id
1489 AND is_paused = false
1490 ",
1491 )?;
1492 let updated = stmt.execute(named_params! {
1493 ":execution_id": execution_id_str,
1494 ":appending_version": appending_version.0,
1495 ":pending_expires_finished": lock_expires_at,
1496 ":state": STATE_LOCKED,
1497 ":deployment_id": deployment_id.to_string(),
1498 ":component_id_input_digest": component_digest,
1499 ":max_retries": retry_config.max_retries,
1500 ":retry_exp_backoff_millis": backoff_millis,
1501 ":executor_id": executor_id.to_string(),
1502 ":run_id": run_id.to_string(),
1503 })?;
1504 if updated != 1 {
1505 return Err(DbErrorWrite::NotFound);
1506 }
1507
1508 let intermittent_event_count = tx
1510 .prepare(
1511 "SELECT intermittent_event_count FROM t_state WHERE execution_id = :execution_id",
1512 )?
1513 .query_row(
1514 named_params! {
1515 ":execution_id": execution_id_str,
1516 },
1517 |row| {
1518 let intermittent_event_count = row.get("intermittent_event_count")?;
1519 Ok(intermittent_event_count)
1520 },
1521 )?;
1522
1523 Ok(intermittent_event_count)
1524 }
1525
1526 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1528 fn update_state_blocked(
1529 tx: &Transaction,
1530 execution_id: &ExecutionId,
1531 appending_version: &Version,
1532 join_set_id: &JoinSetId,
1534 lock_expires_at: DateTime<Utc>,
1535 join_set_closing: bool,
1536 ) -> Result<
1537 AppendResponse, DbErrorWrite,
1539 > {
1540 debug!("Setting t_state to BlockedByJoinSet(`{join_set_id}`)");
1541 let execution_id_str = execution_id.to_string();
1542 let mut stmt = tx.prepare_cached(
1543 r"
1544 UPDATE t_state
1545 SET
1546 corresponding_version = :appending_version,
1547 pending_expires_finished = :pending_expires_finished,
1548 state = :state,
1549 updated_at = CURRENT_TIMESTAMP,
1550
1551 max_retries = NULL,
1552 retry_exp_backoff_millis = NULL,
1553 last_lock_version = NULL,
1554
1555 join_set_id = :join_set_id,
1556 join_set_closing = :join_set_closing,
1557
1558 result_kind = NULL
1559 WHERE execution_id = :execution_id
1560 ",
1561 )?;
1562 let updated = stmt.execute(named_params! {
1563 ":execution_id": execution_id_str,
1564 ":appending_version": appending_version.0,
1565 ":pending_expires_finished": lock_expires_at,
1566 ":state": STATE_BLOCKED_BY_JOIN_SET,
1567 ":join_set_id": join_set_id,
1568 ":join_set_closing": join_set_closing,
1569 })?;
1570 if updated != 1 {
1571 return Err(DbErrorWrite::NotFound);
1572 }
1573 Ok(appending_version.increment())
1574 }
1575
1576 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1577 fn update_state_finished(
1578 tx: &Transaction,
1579 execution_id: &ExecutionId,
1580 appending_version: &Version,
1581 finished_at: DateTime<Utc>,
1583 result_kind: PendingStateFinishedResultKind,
1584 ) -> Result<(), DbErrorWrite> {
1585 debug!("Setting t_state to Finished");
1586 let execution_id_str = execution_id.to_string();
1587 let mut stmt = tx.prepare_cached(
1588 r"
1589 UPDATE t_state
1590 SET
1591 corresponding_version = :appending_version,
1592 pending_expires_finished = :pending_expires_finished,
1593 state = :state,
1594 updated_at = CURRENT_TIMESTAMP,
1595
1596 max_retries = NULL,
1597 retry_exp_backoff_millis = NULL,
1598 last_lock_version = NULL,
1599 executor_id = NULL,
1600 run_id = NULL,
1601
1602 join_set_id = NULL,
1603 join_set_closing = NULL,
1604
1605 result_kind = :result_kind
1606 WHERE execution_id = :execution_id
1607 ",
1608 )?;
1609
1610 let updated = stmt.execute(named_params! {
1611 ":execution_id": execution_id_str,
1612 ":appending_version": appending_version.0,
1613 ":pending_expires_finished": finished_at,
1614 ":state": STATE_FINISHED,
1615 ":result_kind": JsonWrapper(result_kind),
1616 })?;
1617 if updated != 1 {
1618 return Err(DbErrorWrite::NotFound);
1619 }
1620 Ok(())
1621 }
1622
1623 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version, %is_paused))]
1624 fn update_state_paused(
1625 tx: &Transaction,
1626 execution_id: &ExecutionId,
1627 appending_version: &Version,
1628 is_paused: bool,
1629 ) -> Result<AppendResponse, DbErrorWrite> {
1630 debug!(
1631 "Setting t_state to {}",
1632 if is_paused { "paused" } else { "unpaused" }
1633 );
1634 let execution_id_str = execution_id.to_string();
1635 let mut stmt = tx.prepare_cached(
1636 r"
1637 UPDATE t_state
1638 SET
1639 corresponding_version = :appending_version,
1640 is_paused = :is_paused,
1641 updated_at = CURRENT_TIMESTAMP
1642 WHERE execution_id = :execution_id
1643 ",
1644 )?;
1645
1646 let updated = stmt.execute(named_params! {
1647 ":execution_id": execution_id_str,
1648 ":appending_version": appending_version.0,
1649 ":is_paused": is_paused,
1650 })?;
1651 if updated != 1 {
1652 return Err(DbErrorWrite::NotFound);
1653 }
1654 Ok(appending_version.increment())
1655 }
1656
1657 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1659 fn bump_state_next_version(
1660 tx: &Transaction,
1661 execution_id: &ExecutionId,
1662 appending_version: &Version,
1663 delay_req: Option<DelayReq>,
1664 ) -> Result<AppendResponse , DbErrorWrite> {
1665 debug!("update_index_version");
1666 let execution_id_str = execution_id.to_string();
1667 let mut stmt = tx.prepare_cached(
1668 r"
1669 UPDATE t_state
1670 SET
1671 corresponding_version = :appending_version,
1672 updated_at = CURRENT_TIMESTAMP
1673 WHERE execution_id = :execution_id
1674 ",
1675 )?;
1676 let updated = stmt.execute(named_params! {
1677 ":execution_id": execution_id_str,
1678 ":appending_version": appending_version.0,
1679 })?;
1680 if updated != 1 {
1681 return Err(DbErrorWrite::NotFound);
1682 }
1683 if let Some(DelayReq {
1684 join_set_id,
1685 delay_id,
1686 expires_at,
1687 }) = delay_req
1688 {
1689 debug!("Inserting delay to `t_delay`");
1690 let mut stmt = tx.prepare_cached(
1691 "INSERT INTO t_delay (execution_id, join_set_id, delay_id, expires_at) \
1692 VALUES \
1693 (:execution_id, :join_set_id, :delay_id, :expires_at)",
1694 )?;
1695 stmt.execute(named_params! {
1696 ":execution_id": execution_id_str,
1697 ":join_set_id": join_set_id.to_string(),
1698 ":delay_id": delay_id.to_string(),
1699 ":expires_at": expires_at,
1700 })?;
1701 }
1702 Ok(appending_version.increment())
1703 }
1704
1705 fn get_combined_state(
1706 tx: &Transaction,
1707 execution_id: &ExecutionId,
1708 ) -> Result<CombinedState, DbErrorRead> {
1709 let mut stmt = tx.prepare(
1710 r"
1711 SELECT
1712 created_at, first_scheduled_at,
1713 state, ffqn, component_id_input_digest, component_type, deployment_id,
1714 corresponding_version, pending_expires_finished,
1715 last_lock_version, executor_id, run_id,
1716 join_set_id, join_set_closing,
1717 result_kind, is_paused
1718 FROM t_state
1719 WHERE
1720 execution_id = :execution_id
1721 ",
1722 )?;
1723 stmt.query_row(
1724 named_params! {
1725 ":execution_id": execution_id.to_string(),
1726 },
1727 |row| {
1728 CombinedState::new(
1729 CombinedStateDTO {
1730 execution_id: execution_id.clone(),
1731 created_at: row.get("created_at")?,
1732 first_scheduled_at: row.get("first_scheduled_at")?,
1733 component_digest: row.get("component_id_input_digest")?,
1734 component_type: row.get("component_type")?,
1735 deployment_id: row.get("deployment_id")?,
1736 state: row.get("state")?,
1737 ffqn: row.get("ffqn")?,
1738 pending_expires_finished: row
1739 .get::<_, DateTime<Utc>>("pending_expires_finished")?,
1740 last_lock_version: row
1741 .get::<_, Option<VersionType>>("last_lock_version")?
1742 .map(Version::new),
1743 executor_id: row.get::<_, Option<ExecutorId>>("executor_id")?,
1744 run_id: row.get::<_, Option<RunId>>("run_id")?,
1745 join_set_id: row.get::<_, Option<JoinSetId>>("join_set_id")?,
1746 join_set_closing: row.get::<_, Option<bool>>("join_set_closing")?,
1747 result_kind: row
1748 .get::<_, Option<JsonWrapper<PendingStateFinishedResultKind>>>(
1749 "result_kind",
1750 )?
1751 .map(|wrapper| wrapper.0),
1752 is_paused: row.get("is_paused")?,
1753 },
1754 Version::new(row.get("corresponding_version")?),
1755 )
1756 .map_err(|e| from_generic_error(&e))
1757 },
1758 )
1759 .map_err(DbErrorRead::from)
1760 }
1761
1762 fn list_executions(
1763 read_tx: &Transaction,
1764 filter: &ListExecutionsFilter,
1765 pagination: &ExecutionListPagination,
1766 ) -> Result<Vec<ExecutionWithState>, RusqliteError> {
1767 #[derive(Debug)]
1768 struct StatementModifier<'a> {
1769 where_vec: Vec<String>,
1770 params: Vec<(&'static str, ToSqlOutput<'a>)>,
1771 limit: u32,
1772 limit_desc: bool,
1773 }
1774
1775 fn paginate<'a, T: Clone + rusqlite::ToSql + 'static>(
1776 pagination: &'a Pagination<Option<T>>,
1777 column: &str,
1778 filter: &ListExecutionsFilter,
1779 ) -> Result<StatementModifier<'a>, RusqliteError> {
1780 let mut where_vec: Vec<String> = vec![];
1781 let mut params: Vec<(&'static str, ToSqlOutput<'a>)> = vec![];
1782 let limit = pagination.length();
1783 let limit_desc = pagination.is_desc();
1784 match pagination {
1785 Pagination::NewerThan {
1786 cursor: Some(cursor),
1787 ..
1788 }
1789 | Pagination::OlderThan {
1790 cursor: Some(cursor),
1791 ..
1792 } => {
1793 where_vec.push(format!("{column} {rel} :cursor", rel = pagination.rel()));
1794 let cursor = cursor.to_sql().map_err(|err| {
1795 error!("Possible program error - cannot convert cursor to sql - {err:?}");
1796 RusqliteError::Generic {
1797 reason: "cannot convert cursor to sql".into(),
1798 context: SpanTrace::capture(),
1799 source: Some(Arc::new(err)),
1800 loc: Location::caller(),
1801 }
1802 })?;
1803 params.push((":cursor", cursor));
1804 }
1805 _ => {}
1806 }
1807 if !filter.show_derived {
1808 where_vec.push("is_top_level=true".to_string());
1809 }
1810 Ok(StatementModifier {
1811 where_vec,
1812 params,
1813 limit: u32::from(limit),
1814 limit_desc,
1815 })
1816 }
1817
1818 let mut statement_mod = match pagination {
1819 ExecutionListPagination::CreatedBy(pagination) => {
1820 paginate(pagination, "created_at", filter)?
1821 }
1822 ExecutionListPagination::ExecutionId(pagination) => {
1823 paginate(pagination, "execution_id", filter)?
1824 }
1825 };
1826 let like = |str| format!("{str}%");
1827
1828 let ffqn_temporary;
1829 if let Some(ffqn_prefix) = &filter.ffqn_prefix {
1830 statement_mod.where_vec.push("ffqn LIKE :ffqn".to_string());
1831 ffqn_temporary = like(ffqn_prefix);
1832 let ffqn = ffqn_temporary
1833 .to_sql()
1834 .expect("string conversion never fails");
1835 statement_mod.params.push((":ffqn", ffqn));
1836 }
1837
1838 if filter.hide_finished {
1839 statement_mod
1840 .where_vec
1841 .push(format!("state != '{STATE_FINISHED}'"));
1842 }
1843 let prefix_temporary;
1844 if let Some(prefix) = &filter.execution_id_prefix {
1845 statement_mod
1846 .where_vec
1847 .push("execution_id LIKE :prefix".to_string());
1848 prefix_temporary = like(prefix);
1849 statement_mod.params.push((
1850 ":prefix",
1851 prefix_temporary
1852 .to_sql()
1853 .expect("string conversion never fails"),
1854 ));
1855 }
1856
1857 let component_digest_temporary;
1858 if let Some(componnet_digest) = &filter.component_digest {
1859 statement_mod
1860 .where_vec
1861 .push("component_id_input_digest = :component_digest".to_string());
1862 component_digest_temporary = componnet_digest.clone();
1863 let component_digest_sql = component_digest_temporary
1864 .to_sql()
1865 .expect("InputContentDigest conversion never fails");
1866 statement_mod
1867 .params
1868 .push((":component_digest", component_digest_sql));
1869 }
1870
1871 let deployment_id_temporary;
1872 if let Some(deployment_id) = filter.deployment_id {
1873 statement_mod
1874 .where_vec
1875 .push("deployment_id = :deployment_id".to_string());
1876 deployment_id_temporary = deployment_id;
1877 let deployment_id = deployment_id_temporary
1878 .to_sql()
1879 .expect("DeploymentId conversion never fails");
1880 statement_mod.params.push((":deployment_id", deployment_id));
1881 }
1882
1883 let where_str = if statement_mod.where_vec.is_empty() {
1884 String::new()
1885 } else {
1886 format!("WHERE {}", statement_mod.where_vec.join(" AND "))
1887 };
1888
1889 let (inner_order, outer_order) = if statement_mod.limit_desc {
1892 ("DESC", "")
1893 } else {
1894 ("", "DESC")
1895 };
1896
1897 let inner_sql = format!(
1898 r"SELECT created_at, first_scheduled_at, component_id_input_digest, component_type, deployment_id,
1899 state, execution_id, ffqn, corresponding_version, pending_expires_finished,
1900 last_lock_version, executor_id, run_id,
1901 join_set_id, join_set_closing,
1902 result_kind, is_paused
1903 FROM t_state {where_str} ORDER BY created_at {inner_order} LIMIT {limit}",
1904 limit = statement_mod.limit,
1905 );
1906
1907 let sql = if outer_order.is_empty() {
1908 inner_sql
1909 } else {
1910 format!("SELECT * FROM ({inner_sql}) AS sub ORDER BY created_at {outer_order}")
1911 };
1912 let vec: Vec<_> = read_tx
1913 .prepare(&sql)?
1914 .query_map::<_, &[(&'static str, ToSqlOutput)], _>(
1915 statement_mod
1916 .params
1917 .into_iter()
1918 .collect::<Vec<_>>()
1919 .as_ref(),
1920 |row| {
1921 let combined_state = CombinedState::new(
1922 CombinedStateDTO {
1923 execution_id: row.get("execution_id")?,
1924 created_at: row.get("created_at")?,
1925 first_scheduled_at: row.get("first_scheduled_at")?,
1926 component_digest: row.get("component_id_input_digest")?,
1927 component_type: row.get("component_type")?,
1928 deployment_id: row.get("deployment_id")?,
1929 state: row.get("state")?,
1930 ffqn: row.get("ffqn")?,
1931 pending_expires_finished: row.get("pending_expires_finished")?,
1932 executor_id: row.get::<_, Option<ExecutorId>>("executor_id")?,
1933
1934 last_lock_version: row
1935 .get::<_, Option<VersionType>>("last_lock_version")?
1936 .map(Version::new),
1937 run_id: row.get::<_, Option<RunId>>("run_id")?,
1938 join_set_id: row.get::<_, Option<JoinSetId>>("join_set_id")?,
1939 join_set_closing: row.get::<_, Option<bool>>("join_set_closing")?,
1940 result_kind: row
1941 .get::<_, Option<JsonWrapper<PendingStateFinishedResultKind>>>(
1942 "result_kind",
1943 )?
1944 .map(|wrapper| wrapper.0),
1945 is_paused: row.get("is_paused")?,
1946 },
1947 Version::new(row.get("corresponding_version")?),
1948 )
1949 .map_err(|e| from_generic_error(&e))?;
1950 Ok(combined_state.execution_with_state)
1951 },
1952 )?
1953 .collect::<Vec<Result<_, _>>>()
1954 .into_iter()
1955 .filter_map(|row| match row {
1956 Ok(row) => Some(row),
1957 Err(err) => {
1958 warn!("Skipping row - {err:?}");
1959 None
1960 }
1961 })
1962 .collect();
1963
1964 Ok(vec)
1965 }
1966
1967 fn list_responses(
1968 tx: &Transaction,
1969 execution_id: &ExecutionId,
1970 pagination: Option<Pagination<u32>>,
1971 ) -> Result<Vec<ResponseWithCursor>, DbErrorRead> {
1972 let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![];
1974 let mut sql = "SELECT \
1975 r.id, r.created_at, r.join_set_id, r.delay_id, r.delay_success, r.child_execution_id, r.finished_version, l.json_value \
1976 FROM t_join_set_response r LEFT OUTER JOIN t_execution_log l ON r.child_execution_id = l.execution_id \
1977 WHERE \
1978 r.execution_id = :execution_id \
1979 AND ( r.finished_version = l.version OR r.child_execution_id IS NULL ) \
1980 "
1981 .to_string();
1982 let limit = match &pagination {
1983 Some(
1984 pagination @ (Pagination::NewerThan { cursor, .. }
1985 | Pagination::OlderThan { cursor, .. }),
1986 ) => {
1987 params.push((":cursor", Box::new(cursor)));
1988 write!(sql, " AND r.id {rel} :cursor", rel = pagination.rel()).unwrap();
1989 Some(pagination.length())
1990 }
1991 None => None,
1992 };
1993 sql.push_str(" ORDER BY id");
1994 let is_desc = pagination.as_ref().is_some_and(Pagination::is_desc);
1995 if is_desc {
1996 sql.push_str(" DESC");
1997 }
1998 if let Some(limit) = limit {
1999 write!(sql, " LIMIT {limit}").unwrap();
2000 }
2001 if is_desc {
2003 sql = format!("SELECT * FROM ({sql}) ORDER BY id ASC");
2004 }
2005 params.push((":execution_id", Box::new(execution_id.to_string())));
2006 tx.prepare(&sql)?
2007 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
2008 params
2009 .iter()
2010 .map(|(key, value)| (*key, value.as_ref()))
2011 .collect::<Vec<_>>()
2012 .as_ref(),
2013 Self::parse_response_with_cursor,
2014 )?
2015 .collect::<Result<Vec<_>, rusqlite::Error>>()
2016 .map_err(DbErrorRead::from)
2017 }
2018
2019 fn parse_response_with_cursor(
2020 row: &rusqlite::Row<'_>,
2021 ) -> Result<ResponseWithCursor, rusqlite::Error> {
2022 let id = row.get("id")?;
2023 let created_at: DateTime<Utc> = row.get("created_at")?;
2024 let join_set_id = row.get::<_, JoinSetId>("join_set_id")?;
2025 let event = match (
2026 row.get::<_, Option<DelayId>>("delay_id")?,
2027 row.get::<_, Option<bool>>("delay_success")?,
2028 row.get::<_, Option<ExecutionIdDerived>>("child_execution_id")?,
2029 row.get::<_, Option<VersionType>>("finished_version")?,
2030 row.get::<_, Option<JsonWrapper<ExecutionRequest>>>("json_value")?,
2031 ) {
2032 (Some(delay_id), Some(delay_success), None, None, None) => {
2033 JoinSetResponse::DelayFinished {
2034 delay_id,
2035 result: delay_success.then_some(()).ok_or(()),
2036 }
2037 }
2038 (
2039 None,
2040 None,
2041 Some(child_execution_id),
2042 Some(finished_version),
2043 Some(JsonWrapper(ExecutionRequest::Finished { retval: result, .. })),
2044 ) => JoinSetResponse::ChildExecutionFinished {
2045 child_execution_id,
2046 finished_version: Version(finished_version),
2047 result,
2048 },
2049 (delay, delay_success, child, finished, result) => {
2050 error!(
2051 "Invalid row in t_join_set_response {id} - {delay:?} {delay_success:?} {child:?} {finished:?} {:?}",
2052 result.map(|it| it.0)
2053 );
2054 return Err(consistency_rusqlite("invalid row in t_join_set_response"));
2055 }
2056 };
2057 Ok(ResponseWithCursor {
2058 cursor: ResponseCursor(id),
2059 event: JoinSetResponseEventOuter {
2060 event: JoinSetResponseEvent { join_set_id, event },
2061 created_at,
2062 },
2063 })
2064 }
2065
2066 #[instrument(level = Level::TRACE, skip(tx))]
2067 #[expect(clippy::too_many_arguments)]
2068 fn lock_single_execution(
2069 tx: &Transaction,
2070 created_at: DateTime<Utc>,
2071 component_id: &ComponentId,
2072 deployment_id: DeploymentId,
2073 execution_id: &ExecutionId,
2074 run_id: RunId,
2075 appending_version: &Version,
2076 executor_id: ExecutorId,
2077 lock_expires_at: DateTime<Utc>,
2078 retry_config: ComponentRetryConfig,
2079 ) -> Result<LockedExecution, DbErrorWrite> {
2080 trace!("lock_single_execution");
2081 let combined_state = Self::get_combined_state(tx, execution_id)?;
2082 combined_state
2083 .execution_with_state
2084 .pending_state
2085 .can_append_lock(created_at, executor_id, run_id, lock_expires_at)?;
2086 let expected_version = combined_state.get_next_version_assert_not_finished();
2087 Self::check_expected_next_and_appending_version(&expected_version, appending_version)?;
2088
2089 let locked_event = Locked {
2091 component_id: component_id.clone(),
2092 deployment_id,
2093 executor_id,
2094 lock_expires_at,
2095 run_id,
2096 retry_config,
2097 };
2098 let event = ExecutionRequest::Locked(locked_event.clone());
2099 let event_ser = serde_json::to_string(&event).map_err(|err| {
2100 warn!("Cannot serialize {event:?} - {err:?}");
2101 DbErrorWriteNonRetriable::ValidationFailed("parameter serialization error".into())
2102 })?;
2103 let mut stmt = tx
2104 .prepare_cached(
2105 "INSERT INTO t_execution_log \
2106 (execution_id, created_at, json_value, version, variant) \
2107 VALUES \
2108 (:execution_id, :created_at, :json_value, :version, :variant)",
2109 )
2110 .map_err(|err| DbErrorGeneric::Uncategorized {
2111 reason: err.to_string().into(),
2112 context: SpanTrace::capture(),
2113 source: Some(Arc::new(err)),
2114 loc: Location::caller(),
2115 })?;
2116 stmt.execute(named_params! {
2117 ":execution_id": execution_id.to_string(),
2118 ":created_at": created_at,
2119 ":json_value": event_ser,
2120 ":version": appending_version.0,
2121 ":variant": event.variant(),
2122 })
2123 .map_err(|err| {
2124 DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::IllegalState {
2125 reason: "cannot lock".into(),
2126 context: SpanTrace::capture(),
2127 source: Some(Arc::new(err)),
2128 loc: Location::caller(),
2129 })
2130 })?;
2131
2132 let responses = Self::list_responses(tx, execution_id, None)?;
2133 trace!("Responses: {responses:?}");
2134
2135 let intermittent_event_count = Self::update_state_locked_get_intermittent_event_count(
2137 tx,
2138 execution_id,
2139 deployment_id,
2140 &component_id.component_digest,
2141 executor_id,
2142 run_id,
2143 lock_expires_at,
2144 appending_version,
2145 retry_config,
2146 )?;
2147 let mut events = tx
2149 .prepare(
2150 "SELECT json_value, version FROM t_execution_log WHERE \
2151 execution_id = :execution_id AND (variant = :variant1 OR variant = :variant2) \
2152 ORDER BY version",
2153 )?
2154 .query_map(
2155 named_params! {
2156 ":execution_id": execution_id.to_string(),
2157 ":variant1": DUMMY_CREATED.variant(),
2158 ":variant2": DUMMY_HISTORY_EVENT.variant(),
2159 },
2160 |row| {
2161 let created_at_fake = DateTime::from_timestamp_nanos(0); let event = row
2163 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2164 .map_err(|serde| {
2165 error!("Cannot deserialize {row:?} - {serde:?}");
2166 consistency_rusqlite("cannot deserialize event")
2167 })?
2168 .0;
2169 let version = Version(row.get("version")?);
2170
2171 Ok(ExecutionEvent {
2172 created_at: created_at_fake,
2173 event,
2174 backtrace_id: None,
2175 version,
2176 })
2177 },
2178 )?
2179 .collect::<Result<Vec<_>, _>>()?
2180 .into_iter()
2181 .collect::<VecDeque<_>>();
2182 let Some(ExecutionRequest::Created {
2183 ffqn,
2184 params,
2185 parent,
2186 metadata,
2187 ..
2188 }) = events.pop_front().map(|outer| outer.event)
2189 else {
2190 return Err(consistency_db_err("execution log must contain `Created` event").into());
2191 };
2192
2193 let event_history = events
2194 .into_iter()
2195 .map(|ExecutionEvent { event, version, .. }| {
2196 if let ExecutionRequest::HistoryEvent { event } = event {
2197 Ok((event, version))
2198 } else {
2199 Err(consistency_db_err(
2200 "rows can only contain `Created` and `HistoryEvent` event kinds",
2201 ))
2202 }
2203 })
2204 .collect::<Result<Vec<_>, _>>()?;
2205
2206 Ok(LockedExecution {
2207 execution_id: execution_id.clone(),
2208 metadata,
2209 next_version: appending_version.increment(),
2210 ffqn,
2211 params,
2212 event_history,
2213 responses,
2214 parent,
2215 intermittent_event_count,
2216 locked_event,
2217 })
2218 }
2219
2220 fn count_join_next(
2221 tx: &Transaction,
2222 execution_id: &ExecutionId,
2223 join_set_id: &JoinSetId,
2224 ) -> Result<u32, DbErrorRead> {
2225 let mut stmt = tx.prepare(
2226 "SELECT COUNT(*) as count FROM t_execution_log WHERE execution_id = :execution_id AND join_set_id = :join_set_id \
2227 AND history_event_type = :join_next",
2228 )?;
2229 Ok(stmt.query_row(
2230 named_params! {
2231 ":execution_id": execution_id.to_string(),
2232 ":join_set_id": join_set_id.to_string(),
2233 ":join_next": HISTORY_EVENT_TYPE_JOIN_NEXT,
2234 },
2235 |row| row.get::<_, u32>("count"),
2236 )?)
2237 }
2238
2239 fn nth_response(
2240 tx: &Transaction,
2241 execution_id: &ExecutionId,
2242 join_set_id: &JoinSetId,
2243 skip_rows: u32,
2244 ) -> Result<Option<ResponseWithCursor>, DbErrorRead> {
2245 tx
2247 .prepare(
2248 "SELECT r.id, r.created_at, r.join_set_id, \
2249 r.delay_id, r.delay_success, \
2250 r.child_execution_id, r.finished_version, l.json_value \
2251 FROM t_join_set_response r LEFT OUTER JOIN t_execution_log l ON r.child_execution_id = l.execution_id \
2252 WHERE \
2253 r.execution_id = :execution_id AND r.join_set_id = :join_set_id AND \
2254 (
2255 r.finished_version = l.version \
2256 OR \
2257 r.child_execution_id IS NULL \
2258 ) \
2259 ORDER BY id \
2260 LIMIT 1 OFFSET :offset",
2261 )
2262 ?
2263 .query_row(
2264 named_params! {
2265 ":execution_id": execution_id.to_string(),
2266 ":join_set_id": join_set_id.to_string(),
2267 ":offset": skip_rows,
2268 },
2269 Self::parse_response_with_cursor,
2270 )
2271 .optional()
2272 .map_err(DbErrorRead::from)
2273 }
2274
2275 #[instrument(level = Level::TRACE, skip_all, fields(%execution_id, %appending_version))]
2276 #[expect(clippy::needless_return)]
2277 fn append(
2278 tx: &Transaction,
2279 execution_id: &ExecutionId,
2280 req: AppendRequest,
2281 appending_version: Version,
2282 ) -> Result<(AppendResponse, AppendNotifier), DbErrorWrite> {
2283 if matches!(req.event, ExecutionRequest::Created { .. }) {
2284 return Err(DbErrorWrite::NonRetriable(
2285 DbErrorWriteNonRetriable::ValidationFailed(
2286 "cannot append `Created` event - use `create` instead".into(),
2287 ),
2288 ));
2289 }
2290 if let AppendRequest {
2291 event:
2292 ExecutionRequest::Locked(Locked {
2293 component_id,
2294 deployment_id,
2295 executor_id,
2296 run_id,
2297 lock_expires_at,
2298 retry_config,
2299 }),
2300 created_at,
2301 } = req
2302 {
2303 return Self::lock_single_execution(
2304 tx,
2305 created_at,
2306 &component_id,
2307 deployment_id,
2308 execution_id,
2309 run_id,
2310 &appending_version,
2311 executor_id,
2312 lock_expires_at,
2313 retry_config,
2314 )
2315 .map(|locked_execution| (locked_execution.next_version, AppendNotifier::default()));
2316 }
2317
2318 let combined_state = Self::get_combined_state(tx, execution_id)?;
2319 if combined_state
2320 .execution_with_state
2321 .pending_state
2322 .is_finished()
2323 {
2324 debug!("Execution is already finished");
2325 return Err(DbErrorWrite::NonRetriable(
2326 DbErrorWriteNonRetriable::AlreadyFinished,
2327 ));
2328 }
2329
2330 Self::check_expected_next_and_appending_version(
2331 &combined_state.get_next_version_assert_not_finished(),
2332 &appending_version,
2333 )?;
2334 let event_ser = serde_json::to_string(&req.event).map_err(|err| {
2335 error!("Cannot serialize {:?} - {err:?}", req.event);
2336 DbErrorWriteNonRetriable::ValidationFailed("parameter serialization error".into())
2337 })?;
2338
2339 let mut stmt = tx.prepare(
2340 "INSERT INTO t_execution_log (execution_id, created_at, json_value, version, variant, join_set_id) \
2341 VALUES (:execution_id, :created_at, :json_value, :version, :variant, :join_set_id)")
2342 ?;
2343 stmt.execute(named_params! {
2344 ":execution_id": execution_id.to_string(),
2345 ":created_at": req.created_at,
2346 ":json_value": event_ser,
2347 ":version": appending_version.0,
2348 ":variant": req.event.variant(),
2349 ":join_set_id": req.event.join_set_id().map(std::string::ToString::to_string),
2350 })?;
2351 match &req.event {
2354 ExecutionRequest::Created { .. } => {
2355 unreachable!("handled in the caller")
2356 }
2357
2358 ExecutionRequest::Locked { .. } => {
2359 unreachable!("handled above")
2360 }
2361
2362 ExecutionRequest::TemporarilyFailed {
2363 backoff_expires_at, ..
2364 }
2365 | ExecutionRequest::TemporarilyTimedOut {
2366 backoff_expires_at, ..
2367 } => {
2368 let (next_version, notifier) = Self::update_state_pending_after_event_appended(
2369 tx,
2370 execution_id,
2371 &appending_version,
2372 *backoff_expires_at,
2373 true, combined_state.execution_with_state.component_digest,
2375 )?;
2376 return Ok((next_version, notifier));
2377 }
2378
2379 ExecutionRequest::Unlocked {
2380 backoff_expires_at, ..
2381 } => {
2382 let (next_version, notifier) = Self::update_state_pending_after_event_appended(
2383 tx,
2384 execution_id,
2385 &appending_version,
2386 *backoff_expires_at,
2387 false, combined_state.execution_with_state.component_digest,
2389 )?;
2390 return Ok((next_version, notifier));
2391 }
2392
2393 ExecutionRequest::Paused => {
2394 match &combined_state.execution_with_state.pending_state {
2395 PendingState::Finished { .. } => {
2396 unreachable!("handled above");
2397 }
2398 PendingState::Paused(..) => {
2399 return Err(DbErrorWriteNonRetriable::IllegalState {
2400 reason: "cannot pause, execution is already paused".into(),
2401 context: SpanTrace::capture(),
2402 source: None,
2403 loc: Location::caller(),
2404 }
2405 .into());
2406 }
2407 _ => {}
2408 }
2409 let next_version =
2410 Self::update_state_paused(tx, execution_id, &appending_version, true)?;
2411 return Ok((next_version, AppendNotifier::default()));
2412 }
2413
2414 ExecutionRequest::Unpaused => {
2415 if !combined_state
2416 .execution_with_state
2417 .pending_state
2418 .is_paused()
2419 {
2420 return Err(DbErrorWriteNonRetriable::IllegalState {
2421 reason: "cannot unpause, execution is not paused".into(),
2422 context: SpanTrace::capture(),
2423 source: None,
2424 loc: Location::caller(),
2425 }
2426 .into());
2427 }
2428 let next_version =
2429 Self::update_state_paused(tx, execution_id, &appending_version, false)?;
2430 return Ok((next_version, AppendNotifier::default()));
2431 }
2432
2433 ExecutionRequest::Finished { retval, .. } => {
2434 Self::update_state_finished(
2435 tx,
2436 execution_id,
2437 &appending_version,
2438 req.created_at,
2439 PendingStateFinishedResultKind::from(retval),
2440 )?;
2441 return Ok((
2442 appending_version,
2443 AppendNotifier {
2444 pending_at: None,
2445 execution_finished: Some(NotifierExecutionFinished {
2446 execution_id: execution_id.clone(),
2447 retval: retval.clone(),
2448 }),
2449 response: None,
2450 },
2451 ));
2452 }
2453
2454 ExecutionRequest::HistoryEvent {
2455 event:
2456 HistoryEvent::JoinSetCreate { .. }
2457 | HistoryEvent::JoinSetRequest {
2458 request: JoinSetRequest::ChildExecutionRequest { .. },
2459 ..
2460 }
2461 | HistoryEvent::Persist { .. }
2462 | HistoryEvent::Schedule { .. }
2463 | HistoryEvent::Stub { .. }
2464 | HistoryEvent::JoinNextTooMany { .. }
2465 | HistoryEvent::JoinNextTry { .. },
2466 } => {
2467 return Ok((
2468 Self::bump_state_next_version(tx, execution_id, &appending_version, None)?,
2469 AppendNotifier::default(),
2470 ));
2471 }
2472
2473 ExecutionRequest::HistoryEvent {
2474 event:
2475 HistoryEvent::JoinSetRequest {
2476 join_set_id,
2477 request:
2478 JoinSetRequest::DelayRequest {
2479 delay_id,
2480 expires_at,
2481 ..
2482 },
2483 },
2484 } => {
2485 return Ok((
2486 Self::bump_state_next_version(
2487 tx,
2488 execution_id,
2489 &appending_version,
2490 Some(DelayReq {
2491 join_set_id: join_set_id.clone(),
2492 delay_id: delay_id.clone(),
2493 expires_at: *expires_at,
2494 }),
2495 )?,
2496 AppendNotifier::default(),
2497 ));
2498 }
2499
2500 ExecutionRequest::HistoryEvent {
2501 event:
2502 HistoryEvent::JoinNext {
2503 join_set_id,
2504 run_expires_at,
2505 closing,
2506 requested_ffqn: _,
2507 },
2508 } => {
2509 let join_next_count = Self::count_join_next(tx, execution_id, join_set_id)?;
2511 let nth_response =
2512 Self::nth_response(tx, execution_id, join_set_id, join_next_count - 1)?; trace!("join_next_count: {join_next_count}, nth_response: {nth_response:?}");
2514 assert!(join_next_count > 0);
2515 if let Some(ResponseWithCursor {
2516 event:
2517 JoinSetResponseEventOuter {
2518 created_at: nth_created_at,
2519 ..
2520 },
2521 cursor: _,
2522 }) = nth_response
2523 {
2524 let scheduled_at = max(*run_expires_at, nth_created_at); let (next_version, notifier) = Self::update_state_pending_after_event_appended(
2526 tx,
2527 execution_id,
2528 &appending_version,
2529 scheduled_at,
2530 false, combined_state.execution_with_state.component_digest,
2532 )?;
2533 return Ok((next_version, notifier));
2534 }
2535 return Ok((
2536 Self::update_state_blocked(
2537 tx,
2538 execution_id,
2539 &appending_version,
2540 join_set_id,
2541 *run_expires_at,
2542 *closing,
2543 )?,
2544 AppendNotifier::default(),
2545 ));
2546 }
2547 }
2548 }
2549
2550 fn append_response(
2551 tx: &Transaction,
2552 execution_id: &ExecutionId,
2553 event: JoinSetResponseEventOuter,
2554 ) -> Result<AppendNotifier, DbErrorWrite> {
2555 let mut stmt = tx.prepare(
2556 "INSERT INTO t_join_set_response (execution_id, created_at, join_set_id, delay_id, delay_success, child_execution_id, finished_version) \
2557 VALUES (:execution_id, :created_at, :join_set_id, :delay_id, :delay_success, :child_execution_id, :finished_version)",
2558 )?;
2559 let join_set_id = &event.event.join_set_id;
2560 let (delay_id, delay_success) = match &event.event.event {
2561 JoinSetResponse::DelayFinished { delay_id, result } => {
2562 (Some(delay_id.to_string()), Some(result.is_ok()))
2563 }
2564 JoinSetResponse::ChildExecutionFinished { .. } => (None, None),
2565 };
2566 let (child_execution_id, finished_version) = match &event.event.event {
2567 JoinSetResponse::ChildExecutionFinished {
2568 child_execution_id,
2569 finished_version,
2570 result: _,
2571 } => (
2572 Some(child_execution_id.to_string()),
2573 Some(finished_version.0),
2574 ),
2575 JoinSetResponse::DelayFinished { .. } => (None, None),
2576 };
2577
2578 stmt.execute(named_params! {
2579 ":execution_id": execution_id.to_string(),
2580 ":created_at": event.created_at,
2581 ":join_set_id": join_set_id.to_string(),
2582 ":delay_id": delay_id,
2583 ":delay_success": delay_success,
2584 ":child_execution_id": child_execution_id,
2585 ":finished_version": finished_version,
2586 })?;
2587 let cursor = ResponseCursor(
2588 u32::try_from(tx.last_insert_rowid())
2589 .map_err(|_| consistency_db_err("t_join_set_response.id must not be negative"))?,
2590 );
2591
2592 let combined_state = Self::get_combined_state(tx, execution_id)?;
2594 debug!("previous_pending_state: {combined_state:?}");
2595 let mut notifier = if let PendingStateMergedPause::BlockedByJoinSet {
2596 state:
2597 PendingStateBlockedByJoinSet {
2598 join_set_id: found_join_set_id,
2599 lock_expires_at, closing: _,
2601 },
2602 paused: _,
2603 } =
2604 PendingStateMergedPause::from(combined_state.execution_with_state.pending_state)
2605 && *join_set_id == found_join_set_id
2606 {
2607 let scheduled_at = max(lock_expires_at, event.created_at);
2610 Self::update_state_pending_after_response_appended(
2613 tx,
2614 execution_id,
2615 scheduled_at,
2616 combined_state.execution_with_state.component_digest,
2617 )?
2618 } else {
2619 AppendNotifier::default()
2620 };
2621 if let JoinSetResponseEvent {
2622 join_set_id,
2623 event:
2624 JoinSetResponse::DelayFinished {
2625 delay_id,
2626 result: _,
2627 },
2628 } = &event.event
2629 {
2630 debug!(%join_set_id, %delay_id, "Deleting from `t_delay`");
2631 let mut stmt =
2632 tx.prepare_cached("DELETE FROM t_delay WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id")
2633 ?;
2634 stmt.execute(named_params! {
2635 ":execution_id": execution_id.to_string(),
2636 ":join_set_id": join_set_id.to_string(),
2637 ":delay_id": delay_id.to_string(),
2638 })?;
2639 }
2640 notifier.response = Some((execution_id.clone(), ResponseWithCursor { cursor, event }));
2641 Ok(notifier)
2642 }
2643
2644 fn append_backtrace(
2645 tx: &Transaction,
2646 backtrace_info: &BacktraceInfo,
2647 ) -> Result<(), DbErrorWrite> {
2648 let backtrace_hash = backtrace_info.wasm_backtrace.hash();
2649
2650 tx.prepare("INSERT OR IGNORE INTO t_wasm_backtrace (backtrace_hash, wasm_backtrace) VALUES (:backtrace_hash, :wasm_backtrace)")?
2651 .execute(named_params! {
2652 ":backtrace_hash": backtrace_hash,
2653 ":wasm_backtrace": JsonWrapper(&backtrace_info.wasm_backtrace)
2654 })?;
2655
2656 tx.prepare(
2657 "INSERT INTO t_execution_backtrace (execution_id, component_id, version_min_including, version_max_excluding, backtrace_hash) \
2658 VALUES (:execution_id, :component_id, :version_min_including, :version_max_excluding, :backtrace_hash)",
2659 )?
2660 .execute(named_params! {
2661 ":execution_id": backtrace_info.execution_id.to_string(),
2662 ":component_id": JsonWrapper(&backtrace_info.component_id),
2663 ":version_min_including": backtrace_info.version_min_including.0,
2664 ":version_max_excluding": backtrace_info.version_max_excluding.0,
2665 ":backtrace_hash": backtrace_hash,
2666 })?;
2667
2668 Ok(())
2669 }
2670
2671 fn append_log(tx: &Transaction, row: &LogInfoAppendRow) -> Result<(), DbErrorWrite> {
2672 let mut stmt = tx.prepare(
2673 "INSERT INTO t_log (
2674 execution_id,
2675 run_id,
2676 created_at,
2677 level,
2678 message,
2679 stream_type,
2680 payload
2681 ) VALUES (
2682 :execution_id,
2683 :run_id,
2684 :created_at,
2685 :level,
2686 :message,
2687 :stream_type,
2688 :payload
2689 )",
2690 )?;
2691
2692 match &row.log_entry {
2693 LogEntry::Log {
2694 created_at,
2695 level,
2696 message,
2697 } => {
2698 stmt.execute(named_params! {
2699 ":execution_id": row.execution_id,
2700 ":run_id": row.run_id,
2701 ":created_at": created_at,
2702 ":level": *level as u8,
2703 ":message": message,
2704 ":stream_type": Option::<u8>::None,
2705 ":payload": Option::<Vec<u8>>::None,
2706 })?;
2707 }
2708 LogEntry::Stream {
2709 created_at,
2710 payload,
2711 stream_type,
2712 } => {
2713 stmt.execute(named_params! {
2714 ":execution_id": row.execution_id,
2715 ":run_id": row.run_id,
2716 ":created_at": created_at,
2717 ":level": Option::<u8>::None,
2718 ":message": Option::<String>::None,
2719 ":stream_type": *stream_type as u8,
2720 ":payload": payload,
2721 })?;
2722 }
2723 }
2724
2725 Ok(())
2726 }
2727
2728 fn get(
2729 tx: &Transaction,
2730 execution_id: &ExecutionId,
2731 ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
2732 let mut stmt = tx.prepare(
2733 "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2734 execution_id = :execution_id ORDER BY version",
2735 )?;
2736 let events = stmt
2737 .query_map(
2738 named_params! {
2739 ":execution_id": execution_id.to_string(),
2740 },
2741 |row| {
2742 let created_at = row.get("created_at")?;
2743 let event = row
2744 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2745 .map_err(|serde| {
2746 error!("Cannot deserialize {row:?} - {serde:?}");
2747 consistency_rusqlite("cannot deserialize event")
2748 })?
2749 .0;
2750 let version = Version(row.get("version")?);
2751
2752 Ok(ExecutionEvent {
2753 created_at,
2754 event,
2755 backtrace_id: None,
2756 version,
2757 })
2758 },
2759 )?
2760 .collect::<Result<Vec<_>, _>>()?;
2761 if events.is_empty() {
2762 return Err(DbErrorRead::NotFound);
2763 }
2764 let combined_state = Self::get_combined_state(tx, execution_id)?;
2765 let responses = Self::list_responses(tx, execution_id, None)?;
2766 Ok(concepts::storage::ExecutionLog {
2767 execution_id: execution_id.clone(),
2768 events,
2769 responses,
2770 next_version: combined_state.get_next_version_or_finished(), pending_state: combined_state.execution_with_state.pending_state,
2772 component_digest: combined_state.execution_with_state.component_digest,
2773 component_type: combined_state.execution_with_state.component_type,
2774 deployment_id: combined_state.execution_with_state.deployment_id,
2775 })
2776 }
2777
2778 fn get_max_version(
2779 tx: &Transaction,
2780 execution_id: &ExecutionId,
2781 ) -> Result<Version, DbErrorRead> {
2782 tx.prepare("SELECT MAX(version) FROM t_execution_log WHERE execution_id = :execution_id")?
2783 .query_row(
2784 named_params! { ":execution_id": execution_id.to_string() },
2785 |row| row.get::<_, Option<VersionType>>(0),
2786 )
2787 .map(|v| v.map(Version::new).ok_or(DbErrorRead::NotFound))
2788 .map_err(DbErrorRead::from)
2789 .flatten()
2790 }
2791
2792 fn get_max_response_cursor(
2793 tx: &Transaction,
2794 execution_id: &ExecutionId,
2795 ) -> Result<ResponseCursor, DbErrorRead> {
2796 let max_cursor = tx
2797 .prepare("SELECT MAX(id) FROM t_join_set_response WHERE execution_id = :execution_id")?
2798 .query_row(
2799 named_params! { ":execution_id": execution_id.to_string() },
2800 |row| row.get::<_, Option<u32>>(0),
2801 )?;
2802 let max_cursor = max_cursor.unwrap_or_default();
2804 Ok(ResponseCursor(max_cursor))
2805 }
2806
2807 fn list_execution_events(
2808 tx: &Transaction,
2809 execution_id: &ExecutionId,
2810 pagination: Pagination<VersionType>,
2811 include_backtrace_id: bool,
2812 ) -> Result<Vec<ExecutionEvent>, DbErrorRead> {
2813 let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![];
2814 params.push((":execution_id", Box::new(execution_id.to_string())));
2815
2816 let (cursor, length, rel, is_desc) = match &pagination {
2817 Pagination::NewerThan {
2818 cursor,
2819 length,
2820 including_cursor,
2821 } => (
2822 *cursor,
2823 *length,
2824 if *including_cursor { ">=" } else { ">" },
2825 false,
2826 ),
2827 Pagination::OlderThan {
2828 cursor,
2829 length,
2830 including_cursor,
2831 } => (
2832 *cursor,
2833 *length,
2834 if *including_cursor { "<=" } else { "<" },
2835 true,
2836 ),
2837 };
2838 params.push((":cursor", Box::new(cursor)));
2839
2840 let base_select = if include_backtrace_id {
2841 format!(
2842 "SELECT
2843 log.created_at,
2844 log.json_value,
2845 log.version as version,
2846 bt.version_min_including AS backtrace_id
2847 FROM
2848 t_execution_log AS log
2849 LEFT OUTER JOIN
2850 t_execution_backtrace AS bt ON log.execution_id = bt.execution_id
2851 AND log.version >= bt.version_min_including
2852 AND log.version < bt.version_max_excluding
2853 WHERE
2854 log.execution_id = :execution_id
2855 AND log.version {rel} :cursor"
2856 )
2857 } else {
2858 format!(
2859 "SELECT
2860 created_at, json_value, NULL as backtrace_id, version
2861 FROM t_execution_log WHERE
2862 execution_id = :execution_id AND version {rel} :cursor"
2863 )
2864 };
2865
2866 let order = if is_desc { "DESC" } else { "ASC" };
2867 let mut sql = format!("{base_select} ORDER BY version {order} LIMIT {length}");
2868
2869 if is_desc {
2871 sql = format!("SELECT * FROM ({sql}) ORDER BY version ASC");
2872 }
2873
2874 tx.prepare(&sql)?
2875 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
2876 params
2877 .iter()
2878 .map(|(key, value)| (*key, value.as_ref()))
2879 .collect::<Vec<_>>()
2880 .as_ref(),
2881 |row| {
2882 let created_at = row.get("created_at")?;
2883 let backtrace_id = row
2884 .get::<_, Option<VersionType>>("backtrace_id")?
2885 .map(Version::new);
2886 let version = Version(row.get("version")?);
2887
2888 let event = row
2889 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2890 .map(|event| ExecutionEvent {
2891 created_at,
2892 event: event.0,
2893 backtrace_id,
2894 version,
2895 })
2896 .map_err(|serde| {
2897 error!("Cannot deserialize {row:?} - {serde:?}");
2898 consistency_rusqlite("cannot deserialize")
2899 })?;
2900 Ok(event)
2901 },
2902 )?
2903 .collect::<Result<Vec<_>, _>>()
2904 .map_err(DbErrorRead::from)
2905 }
2906
2907 fn map_t_execution_log_row(row: &Row<'_>) -> Result<ExecutionEvent, rusqlite::Error> {
2908 let created_at = row.get("created_at")?;
2909 let event = row
2910 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2911 .map_err(|serde| {
2912 error!("Cannot deserialize {row:?} - {serde:?}");
2913 consistency_rusqlite("cannot deserialize event")
2914 })?;
2915 let version = Version(row.get("version")?);
2916
2917 Ok(ExecutionEvent {
2918 created_at,
2919 event: event.0,
2920 backtrace_id: None,
2921 version,
2922 })
2923 }
2924
2925 fn get_execution_event(
2926 tx: &Transaction,
2927 execution_id: &ExecutionId,
2928 version: VersionType,
2929 ) -> Result<ExecutionEvent, DbErrorRead> {
2930 tx.prepare(
2931 "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2932 execution_id = :execution_id AND version = :version",
2933 )?
2934 .query_row(
2935 named_params! {
2936 ":execution_id": execution_id.to_string(),
2937 ":version": version,
2938 },
2939 SqlitePool::map_t_execution_log_row,
2940 )
2941 .map_err(DbErrorRead::from)
2942 }
2943
2944 fn get_last_execution_event(
2945 tx: &Transaction,
2946 execution_id: &ExecutionId,
2947 ) -> Result<ExecutionEvent, DbErrorRead> {
2948 tx.prepare(
2949 "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2950 execution_id = :execution_id ORDER BY version DESC LIMIT 1",
2951 )?
2952 .query_row(
2953 named_params! {
2954 ":execution_id": execution_id.to_string(),
2955 },
2956 SqlitePool::map_t_execution_log_row,
2957 )
2958 .map_err(DbErrorRead::from)
2959 }
2960
2961 fn get_delay_response(
2962 tx: &Transaction,
2963 execution_id: &ExecutionId,
2964 delay_id: &DelayId,
2965 ) -> Result<Option<bool>, DbErrorRead> {
2966 tx.prepare(
2968 "SELECT delay_success \
2969 FROM t_join_set_response \
2970 WHERE \
2971 execution_id = :execution_id AND delay_id = :delay_id
2972 ",
2973 )?
2974 .query_row(
2975 named_params! {
2976 ":execution_id": execution_id.to_string(),
2977 ":delay_id": delay_id.to_string(),
2978 },
2979 |row| {
2980 let delay_success = row.get::<_, bool>("delay_success")?;
2981 Ok(delay_success)
2982 },
2983 )
2984 .optional()
2985 .map_err(DbErrorRead::from)
2986 }
2987
2988 #[instrument(level = Level::TRACE, skip_all)]
2989 fn get_responses_after(
2991 tx: &Transaction,
2992 execution_id: &ExecutionId,
2993 last_response: ResponseCursor,
2994 ) -> Result<Vec<ResponseWithCursor>, DbErrorRead> {
2995 tx.prepare(
2997 "SELECT r.id, r.created_at, r.join_set_id, \
2998 r.delay_id, r.delay_success, \
2999 r.child_execution_id, r.finished_version, child.json_value \
3000 FROM t_join_set_response r LEFT OUTER JOIN t_execution_log child ON r.child_execution_id = child.execution_id \
3001 WHERE \
3002 r.id > :last_response_id AND \
3003 r.execution_id = :execution_id AND \
3004 ( \
3005 r.finished_version = child.version \
3006 OR r.child_execution_id IS NULL \
3007 ) \
3008 ORDER BY id",
3009 )
3010 ?
3011 .query_map(
3012 named_params! {
3013 ":last_response_id": last_response.0,
3014 ":execution_id": execution_id.to_string(),
3015 },
3016 Self::parse_response_with_cursor,
3017 )
3018 ?
3019 .collect::<Result<Vec<_>, _>>()
3020 .map_err(DbErrorRead::from)
3021 }
3022
3023 fn get_pending_of_single_ffqn(
3024 mut stmt: CachedStatement,
3025 batch_size: u32,
3026 pending_at_or_sooner: DateTime<Utc>,
3027 ffqn: &FunctionFqn,
3028 ) -> Result<Vec<(ExecutionId, Version)>, ()> {
3029 stmt.query_map(
3030 named_params! {
3031 ":pending_expires_finished": pending_at_or_sooner,
3032 ":ffqn": ffqn.to_string(),
3033 ":batch_size": batch_size,
3034 },
3035 |row| {
3036 let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3037 let next_version =
3038 Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3039 Ok((execution_id, next_version))
3040 },
3041 )
3042 .map_err(|err| {
3043 warn!("Ignoring consistency error {err:?}");
3044 })?
3045 .collect::<Result<Vec<_>, _>>()
3046 .map_err(|err| {
3047 warn!("Ignoring consistency error {err:?}");
3048 })
3049 }
3050
3051 fn get_pending_by_ffqns(
3053 conn: &Connection,
3054 batch_size: u32,
3055 pending_at_or_sooner: DateTime<Utc>,
3056 ffqns: &[FunctionFqn],
3057 ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3058 let batch_size = usize::try_from(batch_size).expect("16 bit systems are unsupported");
3059 let mut execution_ids_versions = Vec::with_capacity(batch_size);
3060 for ffqn in ffqns {
3061 let stmt = conn.prepare_cached(&format!(
3063 r#"
3064 SELECT execution_id, corresponding_version FROM t_state WHERE
3065 state = "{STATE_PENDING_AT}" AND
3066 pending_expires_finished <= :pending_expires_finished AND ffqn = :ffqn
3067 AND is_paused = false
3068 ORDER BY pending_expires_finished LIMIT :batch_size
3069 "#
3070 ))?;
3071
3072 if let Ok(execs_and_versions) = Self::get_pending_of_single_ffqn(
3073 stmt,
3074 u32::try_from(batch_size - execution_ids_versions.len())
3075 .expect("u32 - anything must fit to u32"),
3076 pending_at_or_sooner,
3077 ffqn,
3078 ) {
3079 execution_ids_versions.extend(execs_and_versions);
3080 if execution_ids_versions.len() == batch_size {
3081 break;
3083 }
3084 }
3086 }
3087 Ok(execution_ids_versions)
3088 }
3089
3090 fn get_pending_by_component_input_digest(
3091 conn: &Connection,
3092 batch_size: u32,
3093 pending_at_or_sooner: DateTime<Utc>,
3094 input_digest: &ComponentDigest,
3095 ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3096 let mut stmt = conn.prepare_cached(&format!(
3097 r#"
3098 SELECT execution_id, corresponding_version FROM t_state WHERE
3099 state = "{STATE_PENDING_AT}" AND
3100 pending_expires_finished <= :pending_expires_finished AND
3101 component_id_input_digest = :component_id_input_digest
3102 AND is_paused = false
3103 ORDER BY pending_expires_finished LIMIT :batch_size
3104 "#
3105 ))?;
3106
3107 stmt.query_map(
3108 named_params! {
3109 ":pending_expires_finished": pending_at_or_sooner,
3110 ":component_id_input_digest": input_digest,
3111 ":batch_size": batch_size,
3112 },
3113 |row| {
3114 let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3115 let next_version =
3116 Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3117 Ok((execution_id, next_version))
3118 },
3119 )?
3120 .collect::<Result<Vec<_>, _>>()
3121 .map_err(RusqliteError::from)
3122 }
3123
3124 #[instrument(level = Level::TRACE, skip_all)]
3126 fn notify_all(&self, notifiers: Vec<AppendNotifier>, current_time: DateTime<Utc>) {
3127 let (pending_ats, finished_execs, responses) = {
3128 let (mut pending_ats, mut finished_execs, mut responses) =
3129 (Vec::new(), Vec::new(), Vec::new());
3130 for notifier in notifiers {
3131 if let Some(pending_at) = notifier.pending_at {
3132 pending_ats.push(pending_at);
3133 }
3134 if let Some(finished) = notifier.execution_finished {
3135 finished_execs.push(finished);
3136 }
3137 if let Some(response) = notifier.response {
3138 responses.push(response);
3139 }
3140 }
3141 (pending_ats, finished_execs, responses)
3142 };
3143
3144 if !pending_ats.is_empty() {
3146 let guard = self.0.pending_subscribers.lock().unwrap();
3147 for pending_at in pending_ats {
3148 Self::notify_pending_locked(&pending_at, current_time, &guard);
3149 }
3150 }
3151 if !finished_execs.is_empty() {
3154 let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
3155 for finished in finished_execs {
3156 if let Some(listeners_of_exe_id) = guard.remove(&finished.execution_id) {
3157 for (_tag, sender) in listeners_of_exe_id {
3158 let _ = sender.send(finished.retval.clone());
3161 }
3162 }
3163 }
3164 }
3165 if !responses.is_empty() {
3167 let mut guard = self.0.response_subscribers.lock().unwrap();
3168 for (execution_id, response) in responses {
3169 if let Some((sender, _)) = guard.remove(&execution_id) {
3170 let _ = sender.send(response);
3171 }
3172 }
3173 }
3174 }
3175
3176 fn notify_pending_locked(
3177 notifier: &NotifierPendingAt,
3178 current_time: DateTime<Utc>,
3179 ffqn_to_pending_subscription: &std::sync::MutexGuard<PendingFfqnSubscribersHolder>,
3180 ) {
3181 if notifier.scheduled_at <= current_time {
3183 ffqn_to_pending_subscription.notify(notifier);
3184 }
3185 }
3186
3187 fn upgrade_execution_component_single_write(
3188 tx: &Transaction,
3189 execution_id: &ExecutionId,
3190 old: &ComponentDigest,
3191 new: &ComponentDigest,
3192 ) -> Result<(), DbErrorWrite> {
3193 debug!("Updating t_state to component {new}");
3194 let mut stmt = tx.prepare_cached(
3195 r"
3196 UPDATE t_state
3197 SET
3198 updated_at = CURRENT_TIMESTAMP,
3199 component_id_input_digest = :new
3200 WHERE
3201 execution_id = :execution_id AND
3202 component_id_input_digest = :old
3203 ",
3204 )?;
3205 let updated = stmt.execute(named_params! {
3206 ":execution_id": execution_id,
3207 ":old": old,
3208 ":new": new,
3209 })?;
3210 if updated != 1 {
3211 return Err(DbErrorWrite::NotFound);
3212 }
3213 Ok(())
3214 }
3215
3216 fn list_logs_tx(
3217 tx: &Transaction,
3218 execution_id: &ExecutionId,
3219 filter: &LogFilter,
3220 pagination: &Pagination<u32>,
3221 ) -> Result<ListLogsResponse, DbErrorRead> {
3222 let mut query = String::from(
3223 "SELECT id, run_id, created_at, level, message, stream_type, payload
3224 FROM t_log
3225 WHERE execution_id = :execution_id",
3226 );
3227
3228 let length = pagination.length();
3229 let params = vec![
3230 (":execution_id", &execution_id as &dyn rusqlite::ToSql),
3231 (":cursor", pagination.cursor() as &dyn rusqlite::ToSql),
3232 (":length", &length as &dyn rusqlite::ToSql),
3233 ];
3234
3235 let level_filter = if filter.should_show_logs() {
3237 let levels_str = if !filter.levels().is_empty() {
3238 filter
3239 .levels()
3240 .iter()
3241 .map(|lvl| (*lvl as u8).to_string())
3242 .collect::<Vec<_>>()
3243 .join(",")
3244 } else {
3245 LogLevel::iter()
3246 .map(|lvl| (lvl as u8).to_string())
3247 .collect::<Vec<_>>()
3248 .join(",")
3249 };
3250 Some(format!(" level IN ({levels_str})"))
3251 } else {
3252 None
3253 };
3254 let stream_filter = if filter.should_show_streams() {
3255 let streams_str = if !filter.stream_types().is_empty() {
3256 filter
3257 .stream_types()
3258 .iter()
3259 .map(|st| (*st as u8).to_string())
3260 .collect::<Vec<_>>()
3261 .join(",")
3262 } else {
3263 LogStreamType::iter()
3264 .map(|st| (st as u8).to_string())
3265 .collect::<Vec<_>>()
3266 .join(",")
3267 };
3268 Some(format!(" stream_type IN ({streams_str})"))
3269 } else {
3270 None
3271 };
3272 match (level_filter, stream_filter) {
3273 (Some(level_filter), Some(stream_filter)) => {
3274 write!(&mut query, " AND ({level_filter} OR {stream_filter})")
3275 .expect("writing to string");
3276 }
3277 (Some(level_filter), None) => {
3278 write!(&mut query, " AND {level_filter}").expect("writing to string");
3279 }
3280 (None, Some(stream_filter)) => {
3281 write!(&mut query, " AND {stream_filter}").expect("writing to string");
3282 }
3283 (None, None) => unreachable!("guarded by constructor"),
3284 }
3285
3286 write!(&mut query, " AND id {} :cursor", pagination.rel()).expect("writing to string");
3288
3289 query.push_str(" ORDER BY id ");
3291 query.push_str(pagination.asc_or_desc());
3292
3293 query.push_str(" LIMIT :length");
3295
3296 let mut stmt = tx.prepare(&query)?;
3297
3298 let items = stmt
3299 .query_map(params.as_slice(), |row| {
3300 let cursor = row.get("id")?;
3301 let created_at: DateTime<Utc> = row.get("created_at")?;
3302 let run_id = row.get("run_id")?;
3303 let level: Option<u8> = row.get("level")?;
3304 let message: Option<String> = row.get("message")?;
3305 let stream_type: Option<u8> = row.get("stream_type")?;
3306 let payload: Option<Vec<u8>> = row.get("payload")?;
3307
3308 let log_entry = match (level, message, stream_type, payload) {
3309 (Some(lvl), Some(msg), None, None) => LogEntry::Log {
3310 created_at,
3311 level: LogLevel::try_from(lvl).map_err(|_| {
3312 consistency_rusqlite(format!(
3313 "cannot convert {lvl} to LogLevel , id: {cursor}"
3314 ))
3315 })?,
3316 message: msg,
3317 },
3318 (None, None, Some(stype), Some(pl)) => LogEntry::Stream {
3319 created_at,
3320 stream_type: LogStreamType::try_from(stype).map_err(|_| {
3321 consistency_rusqlite(format!(
3322 "cannot convert {stype} to LogStreamType , id: {cursor}"
3323 ))
3324 })?,
3325 payload: pl,
3326 },
3327 _ => {
3328 return Err(consistency_rusqlite(format!(
3329 "invalid t_log row id:{cursor}"
3330 )));
3331 }
3332 };
3333 Ok(LogEntryRow {
3334 cursor,
3335 run_id,
3336 log_entry,
3337 })
3338 })?
3339 .collect::<Result<Vec<_>, _>>()?;
3340
3341 Ok(ListLogsResponse {
3342 next_page: items
3343 .last()
3344 .map(|item| Pagination::NewerThan {
3345 length: pagination.length(),
3346 cursor: item.cursor,
3347 including_cursor: false,
3348 })
3349 .unwrap_or({
3350 if pagination.is_asc() {
3351 *pagination } else {
3353 Pagination::NewerThan {
3355 length: pagination.length(),
3356 cursor: 0,
3357 including_cursor: false, }
3359 }
3360 }),
3361 prev_page: match items.first() {
3362 Some(item) => Some(Pagination::OlderThan {
3363 length: pagination.length(),
3364 cursor: item.cursor,
3365 including_cursor: false,
3366 }),
3367 None if pagination.is_asc() && *pagination.cursor() > 0 => {
3368 Some(pagination.invert())
3370 }
3371 None => None,
3372 },
3373 items,
3374 })
3375 }
3376
3377 fn list_deployment_states(
3378 tx: &Transaction,
3379 current_time: DateTime<Utc>,
3380 pagination: Pagination<Option<DeploymentId>>,
3381 include_config_json: bool,
3382 ) -> Result<Vec<DeploymentState>, DbErrorRead> {
3383 let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3384 let config_json_col = if include_config_json {
3385 "d.config_json"
3386 } else {
3387 "NULL AS config_json"
3388 };
3389 let mut sql = format!(
3390 r"
3391 SELECT
3392 d.deployment_id,
3393 COALESCE(SUM(s.state = '{STATE_LOCKED}'), 0) AS locked,
3394 COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.pending_expires_finished <= :now), 0) AS pending,
3395 COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.pending_expires_finished > :now), 0) AS scheduled,
3396 COALESCE(SUM(s.state = '{STATE_BLOCKED_BY_JOIN_SET}'), 0) AS blocked,
3397 COALESCE(SUM(s.state = '{STATE_FINISHED}'), 0) AS finished,
3398 {config_json_col},
3399 d.created_at,
3400 d.last_active_at,
3401 d.status
3402 FROM t_deployment d
3403 LEFT JOIN t_state s ON s.deployment_id = d.deployment_id"
3404 );
3405
3406 params.push((":now", Box::new(current_time)));
3407
3408 if let Some(cursor) = pagination.cursor() {
3409 params.push((":cursor", Box::new(*cursor)));
3410 write!(
3411 sql,
3412 " WHERE d.deployment_id {rel} :cursor",
3413 rel = pagination.rel()
3414 )
3415 .expect("writing to string");
3416 }
3417
3418 let (inner_order, outer_order) = if pagination.is_desc() {
3421 ("DESC", "")
3422 } else {
3423 ("ASC", "DESC")
3424 };
3425
3426 write!(
3427 sql,
3428 " GROUP BY d.deployment_id, d.config_json, d.created_at, d.last_active_at, d.status ORDER BY d.deployment_id {inner_order} LIMIT {limit}",
3429 limit = pagination.length()
3430 )
3431 .expect("writing to string");
3432
3433 let final_sql = if outer_order.is_empty() {
3434 sql
3435 } else {
3436 format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3437 };
3438
3439 let result: Vec<DeploymentState> = tx
3440 .prepare(&final_sql)?
3441 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3442 params
3443 .iter()
3444 .map(|(k, v)| (*k, v.as_ref()))
3445 .collect::<Vec<_>>()
3446 .as_ref(),
3447 |row| {
3448 let status_str: String = row.get("status")?;
3449 let status = status_str.parse::<DeploymentStatus>().map_err(|_| {
3450 rusqlite::Error::InvalidColumnType(
3451 0,
3452 "status".to_string(),
3453 rusqlite::types::Type::Text,
3454 )
3455 })?;
3456 Ok(DeploymentState {
3457 deployment_id: row.get("deployment_id")?,
3458 locked: row.get("locked")?,
3459 pending: row.get("pending")?,
3460 scheduled: row.get("scheduled")?,
3461 blocked: row.get("blocked")?,
3462 finished: row.get("finished")?,
3463 config_json: row.get("config_json")?,
3464 created_at: row.get("created_at")?,
3465 last_active_at: row.get("last_active_at")?,
3466 status,
3467 })
3468 },
3469 )?
3470 .collect::<Result<Vec<_>, rusqlite::Error>>()
3471 .map_err(DbErrorRead::from)?;
3472
3473 Ok(result)
3474 }
3475
3476 fn insert_deployment_tx(
3477 tx: &Transaction,
3478 record: &DeploymentRecord,
3479 ) -> Result<(), DbErrorWrite> {
3480 assert_eq!(
3481 record.status,
3482 DeploymentStatus::Inactive,
3483 "insert_deployment requires Inactive status"
3484 );
3485 assert!(
3486 record.last_active_at.is_none(),
3487 "insert_deployment requires last_active_at == None"
3488 );
3489 tx.execute(
3490 "INSERT INTO t_deployment \
3491 (deployment_id, created_at, status, config_json, obelisk_version, created_by) \
3492 VALUES (:deployment_id, :created_at, :status, :config_json, :obelisk_version, :created_by)",
3493 rusqlite::named_params! {
3494 ":deployment_id": record.deployment_id.to_string(),
3495 ":created_at": record.created_at,
3496 ":status": record.status.as_str(),
3497 ":config_json": record.config_json,
3498 ":obelisk_version": record.obelisk_version,
3499 ":created_by": record.created_by,
3500 },
3501 )
3502 .map_err(RusqliteError::from)?;
3503 Ok(())
3504 }
3505
3506 fn activate_deployment_tx(
3507 tx: &Transaction,
3508 deployment_id: DeploymentId,
3509 now: DateTime<Utc>,
3510 ) -> Result<(), DbErrorWrite> {
3511 tx.execute(
3513 "UPDATE t_deployment SET status = 'inactive' WHERE status IN ('active', 'enqueued')",
3514 [],
3515 )
3516 .map_err(RusqliteError::from)?;
3517 let rows = tx
3519 .execute(
3520 "UPDATE t_deployment SET status = 'active', last_active_at = :now WHERE deployment_id = :deployment_id",
3521 rusqlite::named_params! {
3522 ":now": now,
3523 ":deployment_id": deployment_id.to_string(),
3524 },
3525 )
3526 .map_err(RusqliteError::from)?;
3527 if rows == 0 {
3528 return Err(DbErrorWrite::NotFound);
3529 }
3530 Ok(())
3531 }
3532
3533 fn enqueue_deployment_tx(
3534 tx: &Transaction,
3535 deployment_id: DeploymentId,
3536 ) -> Result<(), DbErrorWrite> {
3537 let status_opt: Option<String> = tx
3539 .query_row(
3540 "SELECT status FROM t_deployment WHERE deployment_id = :deployment_id",
3541 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3542 |row| row.get(0),
3543 )
3544 .optional()
3545 .map_err(RusqliteError::from)?;
3546 match status_opt.as_deref() {
3547 None => return Err(DbErrorWrite::NotFound),
3548 Some("active") => return Err(DbErrorWriteNonRetriable::Conflict.into()),
3549 _ => {}
3550 }
3551 tx.execute(
3553 "UPDATE t_deployment SET status = 'inactive' WHERE status = 'enqueued'",
3554 [],
3555 )
3556 .map_err(RusqliteError::from)?;
3557 let rows = tx
3559 .execute(
3560 "UPDATE t_deployment SET status = 'enqueued' WHERE deployment_id = :deployment_id",
3561 rusqlite::named_params! {
3562 ":deployment_id": deployment_id.to_string(),
3563 },
3564 )
3565 .map_err(RusqliteError::from)?;
3566 if rows == 0 {
3567 return Err(DbErrorWrite::NotFound);
3568 }
3569 Ok(())
3570 }
3571
3572 fn get_deployment_tx(
3573 tx: &Transaction,
3574 deployment_id: DeploymentId,
3575 ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3576 tx.query_row(
3577 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
3578 FROM t_deployment WHERE deployment_id = :deployment_id",
3579 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3580 deployment_record_from_row,
3581 )
3582 .optional()
3583 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))
3584 }
3585
3586 #[cfg(feature = "test")]
3587 fn get_active_deployment_tx(tx: &Transaction) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3588 tx.query_row(
3589 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
3590 FROM t_deployment WHERE status = 'active' LIMIT 1",
3591 [],
3592 deployment_record_from_row,
3593 )
3594 .optional()
3595 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))
3596 }
3597
3598 fn list_deployments_tx(
3599 tx: &Transaction,
3600 pagination: Pagination<Option<DeploymentId>>,
3601 ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
3602 let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3603 let mut sql = String::from(
3604 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
3605 FROM t_deployment",
3606 );
3607
3608 if let Some(cursor) = pagination.cursor() {
3609 params.push((":cursor", Box::new(*cursor)));
3610 write!(
3611 sql,
3612 " WHERE deployment_id {rel} :cursor",
3613 rel = pagination.rel()
3614 )
3615 .expect("writing to string");
3616 }
3617
3618 let (inner_order, outer_order) = if pagination.is_desc() {
3619 ("DESC", "")
3620 } else {
3621 ("ASC", "DESC")
3622 };
3623
3624 write!(
3625 sql,
3626 " ORDER BY deployment_id {inner_order} LIMIT {limit}",
3627 limit = pagination.length()
3628 )
3629 .expect("writing to string");
3630
3631 let final_sql = if outer_order.is_empty() {
3632 sql
3633 } else {
3634 format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3635 };
3636
3637 let result: Vec<DeploymentRecord> = tx
3638 .prepare(&final_sql)?
3639 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3640 params
3641 .iter()
3642 .map(|(k, v)| (*k, v.as_ref()))
3643 .collect::<Vec<_>>()
3644 .as_ref(),
3645 deployment_record_from_row,
3646 )?
3647 .collect::<Result<Vec<_>, rusqlite::Error>>()
3648 .map_err(DbErrorRead::from)?;
3649
3650 Ok(result)
3651 }
3652
3653 fn pause_execution(
3654 tx: &Transaction,
3655 execution_id: &ExecutionId,
3656 paused_at: DateTime<Utc>,
3657 ) -> Result<Version, DbErrorWrite> {
3658 let combined_state = Self::get_combined_state(tx, execution_id)?;
3659 let appending_version = combined_state.get_next_version_fail_if_finished()?;
3660 debug!("Pausing with {appending_version}");
3661 let (next_version, _) = Self::append(
3662 tx,
3663 execution_id,
3664 AppendRequest {
3665 created_at: paused_at,
3666 event: ExecutionRequest::Paused,
3667 },
3668 appending_version,
3669 )?;
3670 Ok(next_version)
3671 }
3672
3673 fn unpause_execution(
3674 tx: &Transaction,
3675 execution_id: &ExecutionId,
3676 paused_at: DateTime<Utc>,
3677 ) -> Result<Version, DbErrorWrite> {
3678 let combined_state = Self::get_combined_state(tx, execution_id)?;
3679 let appending_version = combined_state.get_next_version_fail_if_finished()?;
3680 debug!("Unpausing with {appending_version}");
3681 let (next_version, _) = Self::append(
3682 tx,
3683 execution_id,
3684 AppendRequest {
3685 created_at: paused_at,
3686 event: ExecutionRequest::Unpaused,
3687 },
3688 appending_version,
3689 )?;
3690 Ok(next_version)
3691 }
3692}
3693
3694#[async_trait]
3695impl DbExecutor for SqlitePool {
3696 #[instrument(level = Level::TRACE, skip(self))]
3697 async fn lock_pending_by_ffqns(
3698 &self,
3699 batch_size: u32,
3700 pending_at_or_sooner: DateTime<Utc>,
3701 ffqns: Arc<[FunctionFqn]>,
3702 created_at: DateTime<Utc>,
3703 component_id: ComponentId,
3704 deployment_id: DeploymentId,
3705 executor_id: ExecutorId,
3706 lock_expires_at: DateTime<Utc>,
3707 run_id: RunId,
3708 retry_config: ComponentRetryConfig,
3709 ) -> Result<LockPendingResponse, DbErrorWrite> {
3710 let execution_ids_versions = self
3711 .transaction(
3712 move |conn| {
3713 Self::get_pending_by_ffqns(conn, batch_size, pending_at_or_sooner, &ffqns)
3714 },
3715 TxType::Other, "lock_pending_by_ffqns_get",
3717 )
3718 .await
3719 .map_err(to_generic_error)?;
3720 if execution_ids_versions.is_empty() {
3721 Ok(vec![])
3722 } else {
3723 debug!("Locking {execution_ids_versions:?}");
3724 self.transaction(
3725 move |tx| {
3726 let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
3727 for (execution_id, version) in &execution_ids_versions {
3729 locked_execs.push(Self::lock_single_execution(
3730 tx,
3731 created_at,
3732 &component_id,
3733 deployment_id,
3734 execution_id,
3735 run_id,
3736 version,
3737 executor_id,
3738 lock_expires_at,
3739 retry_config,
3740 )?);
3741 }
3742 Ok::<_, DbErrorWrite>(locked_execs)
3743 },
3744 TxType::MultipleWrites,
3745 "lock_pending_by_ffqns_one",
3746 )
3747 .await
3748 }
3749 }
3750
3751 #[instrument(level = Level::TRACE, skip(self))]
3752 async fn lock_pending_by_component_digest(
3753 &self,
3754 batch_size: u32,
3755 pending_at_or_sooner: DateTime<Utc>,
3756 component_id: &ComponentId,
3757 deployment_id: DeploymentId,
3758 created_at: DateTime<Utc>,
3759 executor_id: ExecutorId,
3760 lock_expires_at: DateTime<Utc>,
3761 run_id: RunId,
3762 retry_config: ComponentRetryConfig,
3763 ) -> Result<LockPendingResponse, DbErrorWrite> {
3764 let component_id = component_id.clone();
3765 let execution_ids_versions = self
3766 .transaction(
3767 {
3768 let component_id = component_id.clone();
3769 move |conn| {
3770 Self::get_pending_by_component_input_digest(
3771 conn,
3772 batch_size,
3773 pending_at_or_sooner,
3774 &component_id.component_digest,
3775 )
3776 }
3777 },
3778 TxType::Other, "lock_pending_by_component_id_get",
3780 )
3781 .await
3782 .map_err(to_generic_error)?;
3783 if execution_ids_versions.is_empty() {
3784 Ok(vec![])
3785 } else {
3786 debug!("Locking {execution_ids_versions:?}");
3787 self.transaction(
3788 move |tx| {
3789 let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
3790 for (execution_id, version) in &execution_ids_versions {
3792 locked_execs.push(Self::lock_single_execution(
3793 tx,
3794 created_at,
3795 &component_id,
3796 deployment_id,
3797 execution_id,
3798 run_id,
3799 version,
3800 executor_id,
3801 lock_expires_at,
3802 retry_config,
3803 )?);
3804 }
3805 Ok::<_, DbErrorWrite>(locked_execs)
3806 },
3807 TxType::MultipleWrites,
3808 "lock_pending_by_component_id_one",
3809 )
3810 .await
3811 }
3812 }
3813
3814 #[cfg(feature = "test")]
3815 #[instrument(level = Level::DEBUG, skip(self))]
3816 async fn lock_one(
3817 &self,
3818 created_at: DateTime<Utc>,
3819 component_id: ComponentId,
3820 deployment_id: DeploymentId,
3821 execution_id: &ExecutionId,
3822 run_id: RunId,
3823 version: Version,
3824 executor_id: ExecutorId,
3825 lock_expires_at: DateTime<Utc>,
3826 retry_config: ComponentRetryConfig,
3827 ) -> Result<LockedExecution, DbErrorWrite> {
3828 debug!(%execution_id, "lock_one");
3829 let execution_id = execution_id.clone();
3830 self.transaction(
3831 move |tx| {
3832 Self::lock_single_execution(
3833 tx,
3834 created_at,
3835 &component_id,
3836 deployment_id,
3837 &execution_id,
3838 run_id,
3839 &version,
3840 executor_id,
3841 lock_expires_at,
3842 retry_config,
3843 )
3844 },
3845 TxType::MultipleWrites, "lock_inner",
3847 )
3848 .await
3849 }
3850
3851 #[instrument(level = Level::DEBUG, skip(self, req))]
3852 async fn append(
3853 &self,
3854 execution_id: ExecutionId,
3855 version: Version,
3856 req: AppendRequest,
3857 ) -> Result<AppendResponse, DbErrorWrite> {
3858 debug!(%req, "append");
3859 trace!(?req, "append");
3860 let created_at = req.created_at;
3861 let (version, notifier) = self
3862 .transaction(
3863 move |tx| Self::append(tx, &execution_id, req.clone(), version.clone()),
3864 TxType::MultipleWrites, "append",
3866 )
3867 .await?;
3868 self.notify_all(vec![notifier], created_at);
3869 Ok(version)
3870 }
3871
3872 #[instrument(level = Level::DEBUG, skip_all)]
3873 async fn append_batch_respond_to_parent(
3874 &self,
3875 events: AppendEventsToExecution,
3876 response: AppendResponseToExecution,
3877 current_time: DateTime<Utc>,
3878 ) -> Result<AppendBatchResponse, DbErrorWrite> {
3879 debug!("append_batch_respond_to_parent");
3880 if events.execution_id == response.parent_execution_id {
3881 return Err(DbErrorWrite::NonRetriable(
3884 DbErrorWriteNonRetriable::ValidationFailed(
3885 "Parameters `execution_id` and `parent_execution_id` cannot be the same".into(),
3886 ),
3887 ));
3888 }
3889 if events.batch.is_empty() {
3890 error!("Batch cannot be empty");
3891 return Err(DbErrorWrite::NonRetriable(
3892 DbErrorWriteNonRetriable::ValidationFailed("batch cannot be empty".into()),
3893 ));
3894 }
3895 let (version, notifiers) = {
3896 self.transaction(
3897 move |tx| {
3898 let mut version = events.version.clone();
3899 let mut notifier_of_child = None;
3900 for append_request in &events.batch {
3901 let (v, n) = Self::append(
3902 tx,
3903 &events.execution_id,
3904 append_request.clone(),
3905 version,
3906 )?;
3907 version = v;
3908 notifier_of_child = Some(n);
3909 }
3910
3911 let pending_at_parent = Self::append_response(
3912 tx,
3913 &response.parent_execution_id,
3914 JoinSetResponseEventOuter {
3915 created_at: response.created_at,
3916 event: JoinSetResponseEvent {
3917 join_set_id: response.join_set_id.clone(),
3918 event: JoinSetResponse::ChildExecutionFinished {
3919 child_execution_id: response.child_execution_id.clone(),
3920 finished_version: response.finished_version.clone(),
3921 result: response.result.clone(),
3922 },
3923 },
3924 },
3925 )?;
3926 Ok::<_, DbErrorWrite>((
3927 version,
3928 vec![
3929 notifier_of_child.expect("checked that the batch is not empty"),
3930 pending_at_parent,
3931 ],
3932 ))
3933 },
3934 TxType::MultipleWrites,
3935 "append_batch_respond_to_parent",
3936 )
3937 .await?
3938 };
3939 self.notify_all(notifiers, current_time);
3940 Ok(version)
3941 }
3942
3943 #[instrument(level = Level::TRACE, skip(self, timeout_fut))]
3946 async fn wait_for_pending_by_ffqn(
3947 &self,
3948 pending_at_or_sooner: DateTime<Utc>,
3949 ffqns: Arc<[FunctionFqn]>,
3950 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
3951 ) {
3952 let unique_tag: u64 = rand::random();
3953 let (sender, mut receiver) = mpsc::channel(1); {
3955 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
3956 for ffqn in ffqns.as_ref() {
3957 pending_subscribers.insert_ffqn(ffqn.clone(), (sender.clone(), unique_tag));
3958 }
3959 }
3960 async {
3961 let Ok(execution_ids_versions) = self
3962 .transaction(
3963 {
3964 let ffqns = ffqns.clone();
3965 move |conn| Self::get_pending_by_ffqns(conn, 1, pending_at_or_sooner, ffqns.as_ref())
3966 },
3967 TxType::Other, "get_pending_by_ffqns",
3969 )
3970 .await
3971 else {
3972 trace!(
3973 "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
3974 );
3975 timeout_fut.await;
3976 return;
3977 };
3978 if !execution_ids_versions.is_empty() {
3979 trace!("Not waiting, database already contains new pending executions");
3980 return;
3981 }
3982 tokio::select! { _ = receiver.recv() => {
3984 trace!("Received a notification");
3985 }
3986 () = timeout_fut => {
3987 }
3988 }
3989 }.await;
3990 {
3992 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
3993 for ffqn in ffqns.as_ref() {
3994 match pending_subscribers.remove_ffqn(ffqn) {
3995 Some((_, tag)) if tag == unique_tag => {
3996 }
3998 Some(other) => {
3999 pending_subscribers.insert_ffqn(ffqn.clone(), other);
4001 }
4002 None => {
4003 }
4005 }
4006 }
4007 }
4008 }
4009
4010 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4013 async fn wait_for_pending_by_component_digest(
4014 &self,
4015 pending_at_or_sooner: DateTime<Utc>,
4016 component_digest: &ComponentDigest,
4017 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4018 ) {
4019 let unique_tag: u64 = rand::random();
4020 let (sender, mut receiver) = mpsc::channel(1); {
4022 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4023 pending_subscribers
4024 .insert_by_component(component_digest.clone(), (sender.clone(), unique_tag));
4025 }
4026 async {
4027 let Ok(execution_ids_versions) = self
4028 .transaction(
4029 {
4030 let input_digest = component_digest.clone();
4031 move |conn| Self::get_pending_by_component_input_digest(conn, 1, pending_at_or_sooner, &input_digest)
4032 },
4033 TxType::Other, "get_pending_by_component_input_digest",
4035 )
4036 .await
4037 else {
4038 trace!(
4039 "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4040 );
4041 timeout_fut.await;
4042 return;
4043 };
4044 if !execution_ids_versions.is_empty() {
4045 trace!("Not waiting, database already contains new pending executions");
4046 return;
4047 }
4048 tokio::select! { _ = receiver.recv() => {
4050 trace!("Received a notification");
4051 }
4052 () = timeout_fut => {
4053 }
4054 }
4055 }.await;
4056 {
4058 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4059
4060 match pending_subscribers.remove_by_component(component_digest) {
4061 Some((_, tag)) if tag == unique_tag => {
4062 }
4064 Some(other) => {
4065 pending_subscribers.insert_by_component(component_digest.clone(), other);
4067 }
4068 None => {
4069 }
4071 }
4072 }
4073 }
4074
4075 async fn get_last_execution_event(
4076 &self,
4077 execution_id: &ExecutionId,
4078 ) -> Result<ExecutionEvent, DbErrorRead> {
4079 let execution_id = execution_id.clone();
4080 self.transaction(
4081 move |tx| Self::get_last_execution_event(tx, &execution_id),
4082 TxType::Other, "get_last_execution_event",
4084 )
4085 .await
4086 }
4087}
4088
4089#[async_trait]
4090impl DbExternalApi for SqlitePool {
4091 #[instrument(skip(self))]
4092 async fn get_backtrace(
4093 &self,
4094 execution_id: &ExecutionId,
4095 filter: BacktraceFilter,
4096 ) -> Result<BacktraceInfo, DbErrorRead> {
4097 debug!("get_backtrace");
4098 let execution_id = execution_id.clone();
4099
4100 self.transaction(
4101 move |tx| {
4102 let select = "SELECT component_id, version_min_including, version_max_excluding, wasm_backtrace FROM t_execution_backtrace e \
4103 INNER JOIN t_wasm_backtrace w ON e.backtrace_hash = w.backtrace_hash \
4104 WHERE execution_id = :execution_id";
4105 let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![(":execution_id", Box::new(execution_id.to_string()))];
4106 let select = match &filter {
4107 BacktraceFilter::Specific(version) =>{
4108 params.push((":version", Box::new(version.0)));
4109 format!("{select} AND version_min_including <= :version AND version_max_excluding > :version")
4110 },
4111 BacktraceFilter::First => format!("{select} ORDER BY version_min_including LIMIT 1"),
4112 BacktraceFilter::Last => format!("{select} ORDER BY version_min_including DESC LIMIT 1")
4113 };
4114 tx
4115 .prepare(&select)
4116 ?
4117 .query_row::<_, &[(&'static str, &dyn ToSql)], _>(
4118 params
4119 .iter()
4120 .map(|(key, value)| (*key, value.as_ref()))
4121 .collect::<Vec<_>>()
4122 .as_ref(),
4123 |row| {
4124 Ok(BacktraceInfo {
4125 execution_id: execution_id.clone(),
4126 component_id: row.get::<_, JsonWrapper<_> >("component_id")?.0,
4127 version_min_including: Version::new(row.get::<_, VersionType>("version_min_including")?),
4128 version_max_excluding: Version::new(row.get::<_, VersionType>("version_max_excluding")?),
4129 wasm_backtrace: row.get::<_, JsonWrapper<_>>("wasm_backtrace")?.0,
4130 })
4131 },
4132 ).map_err(DbErrorRead::from)
4133 },
4134 TxType::Other, "get_last_backtrace",
4136 ).await
4137 }
4138
4139 #[instrument(skip_all)]
4140 async fn upsert_source_file(
4141 &self,
4142 component_digest: &ComponentDigest,
4143 frame_key: &str,
4144 is_suffix: bool,
4145 content: &str,
4146 ) -> Result<(), DbErrorWrite> {
4147 let content_hash: [u8; 32] = Sha256::digest(content.as_bytes()).into();
4148 let component_digest = component_digest.clone();
4149 let frame_key = frame_key.to_owned();
4150 let content = content.to_owned();
4151 self.transaction(
4152 move |tx| {
4153 tx.prepare(
4154 "INSERT OR IGNORE INTO t_source_file (content_hash, content) \
4155 VALUES (:content_hash, :content)",
4156 )?
4157 .execute(named_params! {
4158 ":content_hash": content_hash,
4159 ":content": content,
4160 })?;
4161 tx.prepare(
4162 "INSERT OR IGNORE INTO t_component_source \
4163 (component_digest, frame_key, is_suffix, content_hash) \
4164 VALUES (:component_digest, :frame_key, :is_suffix, :content_hash)",
4165 )?
4166 .execute(named_params! {
4167 ":component_digest": component_digest,
4168 ":frame_key": frame_key,
4169 ":is_suffix": is_suffix,
4170 ":content_hash": content_hash,
4171 })?;
4172 Ok(())
4173 },
4174 TxType::Other,
4175 "upsert_source_file",
4176 )
4177 .await
4178 }
4179
4180 #[instrument(skip_all)]
4181 async fn get_source_file(
4182 &self,
4183 component_digest: &ComponentDigest,
4184 file: &str,
4185 ) -> Result<Option<String>, DbErrorRead> {
4186 let component_digest = component_digest.clone();
4187 let file = file.to_owned();
4188 self.transaction(
4189 move |tx| {
4190 let mut stmt = tx.prepare(
4191 "SELECT s.content \
4192 FROM t_component_source cs \
4193 JOIN t_source_file s ON cs.content_hash = s.content_hash \
4194 WHERE cs.component_digest = :component_digest \
4195 AND ( \
4196 (cs.is_suffix = 0 AND cs.frame_key = :file) \
4197 OR (cs.is_suffix = 1 AND \
4198 substr(:file, length(:file) - length(cs.frame_key) + 1) = cs.frame_key) \
4199 )",
4200 )?;
4201 let rows: Vec<String> = stmt
4202 .query_map(
4203 named_params! {
4204 ":component_digest": component_digest,
4205 ":file": file,
4206 },
4207 |row| row.get(0),
4208 )?
4209 .collect::<Result<_, _>>()?;
4210 match rows.len() {
4211 0 => Ok(None),
4212 1 => Ok(Some(rows.into_iter().next().unwrap())),
4213 _ => {
4214 warn!("Multiple suffix matches for '{file}', returning None");
4215 Ok(None)
4216 }
4217 }
4218 },
4219 TxType::Other,
4220 "get_source_file",
4221 )
4222 .await
4223 }
4224
4225 #[instrument(skip(self))]
4226 async fn list_executions(
4227 &self,
4228 filter: ListExecutionsFilter,
4229 pagination: ExecutionListPagination,
4230 ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric> {
4231 self.transaction(
4232 move |tx| Self::list_executions(tx, &filter, &pagination),
4233 TxType::Other, "list_executions",
4235 )
4236 .await
4237 .map_err(to_generic_error)
4238 }
4239
4240 #[instrument(skip(self))]
4241 async fn list_execution_events(
4242 &self,
4243 execution_id: &ExecutionId,
4244 pagination: Pagination<VersionType>,
4245 include_backtrace_id: bool,
4246 ) -> Result<ListExecutionEventsResponse, DbErrorRead> {
4247 let execution_id = execution_id.clone();
4248 self.transaction(
4249 move |tx| {
4250 let events = Self::list_execution_events(
4251 tx,
4252 &execution_id,
4253 pagination,
4254 include_backtrace_id,
4255 )?;
4256 let max_version = Self::get_max_version(tx, &execution_id)?;
4257 Ok(ListExecutionEventsResponse {
4258 events,
4259 max_version,
4260 })
4261 },
4262 TxType::Other, "get",
4264 )
4265 .await
4266 }
4267
4268 #[instrument(skip(self))]
4269 async fn list_responses(
4270 &self,
4271 execution_id: &ExecutionId,
4272 pagination: Pagination<u32>,
4273 ) -> Result<ListResponsesResponse, DbErrorRead> {
4274 let execution_id = execution_id.clone();
4275 self.transaction(
4276 move |tx| {
4277 let responses = Self::list_responses(tx, &execution_id, Some(pagination))?;
4278 let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4279 Ok(ListResponsesResponse {
4280 responses,
4281 max_cursor,
4282 })
4283 },
4284 TxType::Other, "list_responses",
4286 )
4287 .await
4288 }
4289
4290 #[instrument(skip(self))]
4291 async fn list_execution_events_responses(
4292 &self,
4293 execution_id: &ExecutionId,
4294 req_since: &Version,
4295 req_max_length: VersionType,
4296 req_include_backtrace_id: bool,
4297 resp_pagination: Pagination<u32>,
4298 ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead> {
4299 let execution_id = execution_id.clone();
4300 let req_since = req_since.0;
4301 self.transaction(
4302 move |tx| {
4303 let combined_state = Self::get_combined_state(tx, &execution_id)?;
4304 let events = Self::list_execution_events(
4305 tx,
4306 &execution_id,
4307 Pagination::NewerThan {
4308 length: req_max_length
4309 .try_into()
4310 .expect("req_max_length fits in u16"),
4311 cursor: req_since,
4312 including_cursor: true,
4313 },
4314 req_include_backtrace_id,
4315 )?;
4316 let responses = Self::list_responses(tx, &execution_id, Some(resp_pagination))?;
4317 let max_version = Self::get_max_version(tx, &execution_id)?;
4318 let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4319 Ok(ExecutionWithStateRequestsResponses {
4320 execution_with_state: combined_state.execution_with_state,
4321 events,
4322 responses,
4323 max_version,
4324 max_cursor,
4325 })
4326 },
4327 TxType::Other, "list_execution_events_responses",
4329 )
4330 .await
4331 }
4332
4333 #[instrument(skip(self))]
4334 async fn upgrade_execution_component(
4335 &self,
4336 execution_id: &ExecutionId,
4337 old: &ComponentDigest,
4338 new: &ComponentDigest,
4339 ) -> Result<(), DbErrorWrite> {
4340 let execution_id = execution_id.clone();
4341 let old = old.clone();
4342 let new = new.clone();
4343 self.transaction(
4344 move |tx| Self::upgrade_execution_component_single_write(tx, &execution_id, &old, &new),
4345 TxType::Other, "upgrade_execution_component",
4347 )
4348 .await
4349 }
4350
4351 #[instrument(skip(self))]
4352 async fn list_logs(
4353 &self,
4354 execution_id: &ExecutionId,
4355 filter: LogFilter,
4356 pagination: Pagination<u32>,
4357 ) -> Result<ListLogsResponse, DbErrorRead> {
4358 let execution_id = execution_id.clone();
4359 self.transaction(
4360 move |tx| Self::list_logs_tx(tx, &execution_id, &filter, &pagination),
4361 TxType::Other, "list_logs",
4363 )
4364 .await
4365 }
4366
4367 #[instrument(skip(self))]
4368 async fn list_deployment_states(
4369 &self,
4370 current_time: DateTime<Utc>,
4371 pagination: Pagination<Option<DeploymentId>>,
4372 include_config_json: bool,
4373 ) -> Result<Vec<DeploymentState>, DbErrorRead> {
4374 self.transaction(
4375 move |tx| {
4376 Self::list_deployment_states(tx, current_time, pagination, include_config_json)
4377 },
4378 TxType::Other, "list_deployment_states",
4380 )
4381 .await
4382 }
4383
4384 #[instrument(skip(self))]
4385 async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite> {
4386 self.transaction(
4387 move |tx| Self::insert_deployment_tx(tx, &record),
4388 TxType::MultipleWrites,
4389 "insert_deployment",
4390 )
4391 .await
4392 }
4393
4394 #[instrument(skip(self))]
4395 async fn activate_deployment(
4396 &self,
4397 deployment_id: DeploymentId,
4398 now: DateTime<Utc>,
4399 ) -> Result<(), DbErrorWrite> {
4400 self.transaction(
4401 move |tx| Self::activate_deployment_tx(tx, deployment_id, now),
4402 TxType::MultipleWrites,
4403 "activate_deployment",
4404 )
4405 .await
4406 }
4407
4408 async fn enqueue_deployment(&self, deployment_id: DeploymentId) -> Result<(), DbErrorWrite> {
4409 self.transaction(
4410 move |tx| Self::enqueue_deployment_tx(tx, deployment_id),
4411 TxType::MultipleWrites,
4412 "enqueue_deployment",
4413 )
4414 .await
4415 }
4416
4417 #[instrument(skip(self))]
4418 async fn get_deployment(
4419 &self,
4420 deployment_id: DeploymentId,
4421 ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
4422 self.transaction(
4423 move |tx| Self::get_deployment_tx(tx, deployment_id),
4424 TxType::Other,
4425 "get_deployment",
4426 )
4427 .await
4428 }
4429
4430 #[cfg(feature = "test")]
4431 #[instrument(skip(self))]
4432 async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
4433 self.transaction(
4434 move |tx| Self::get_active_deployment_tx(tx),
4435 TxType::Other,
4436 "get_active_deployment",
4437 )
4438 .await
4439 }
4440
4441 #[instrument(skip(self))]
4442 async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
4443 self.transaction(
4444 move |tx| {
4445 tx.query_row(
4446 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
4447 FROM t_deployment WHERE status IN ('enqueued', 'active') \
4448 ORDER BY CASE status WHEN 'enqueued' THEN 0 ELSE 1 END LIMIT 1",
4449 [],
4450 deployment_record_from_row,
4451 )
4452 .optional()
4453 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))
4454 },
4455 TxType::Other,
4456 "get_current_deployment",
4457 )
4458 .await
4459 }
4460
4461 #[instrument(skip(self))]
4462 async fn list_deployments(
4463 &self,
4464 pagination: Pagination<Option<DeploymentId>>,
4465 ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
4466 self.transaction(
4467 move |tx| Self::list_deployments_tx(tx, pagination),
4468 TxType::Other,
4469 "list_deployments",
4470 )
4471 .await
4472 }
4473
4474 #[instrument(skip(self))]
4475 async fn pause_execution(
4476 &self,
4477 execution_id: &ExecutionId,
4478 paused_at: DateTime<Utc>,
4479 ) -> Result<AppendResponse, DbErrorWrite> {
4480 let execution_id = execution_id.clone();
4481 self.transaction(
4482 move |tx| SqlitePool::pause_execution(tx, &execution_id, paused_at),
4483 TxType::MultipleWrites,
4484 "pause_execution",
4485 )
4486 .await
4487 }
4488
4489 #[instrument(skip(self))]
4490 async fn unpause_execution(
4491 &self,
4492 execution_id: &ExecutionId,
4493 unpaused_at: DateTime<Utc>,
4494 ) -> Result<AppendResponse, DbErrorWrite> {
4495 let execution_id = execution_id.clone();
4496 self.transaction(
4497 move |tx| SqlitePool::unpause_execution(tx, &execution_id, unpaused_at),
4498 TxType::MultipleWrites,
4499 "unpause_execution",
4500 )
4501 .await
4502 }
4503}
4504
4505#[async_trait]
4506impl DbConnection for SqlitePool {
4507 #[instrument(level = Level::DEBUG, skip_all, fields(execution_id = %req.execution_id))]
4508 async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite> {
4509 debug!("create");
4510 trace!(?req, "create");
4511 let created_at = req.created_at;
4512 let (version, notifier) = self
4513 .transaction(
4514 move |tx| Self::create_inner(tx, req.clone()),
4515 TxType::MultipleWrites,
4516 "create",
4517 )
4518 .await?;
4519 self.notify_all(vec![notifier], created_at);
4520 Ok(version)
4521 }
4522
4523 #[instrument(level = Level::DEBUG, skip(self))]
4524 async fn get(
4525 &self,
4526 execution_id: &ExecutionId,
4527 ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
4528 trace!("get");
4529 let execution_id = execution_id.clone();
4530 self.transaction(
4531 move |tx| Self::get(tx, &execution_id),
4532 TxType::Other, "get",
4534 )
4535 .await
4536 }
4537
4538 #[instrument(level = Level::DEBUG, skip(self, batch))]
4539 async fn append_batch(
4540 &self,
4541 current_time: DateTime<Utc>,
4542 batch: Vec<AppendRequest>,
4543 execution_id: ExecutionId,
4544 version: Version,
4545 ) -> Result<AppendBatchResponse, DbErrorWrite> {
4546 debug!("append_batch");
4547 trace!(?batch, "append_batch");
4548 assert!(!batch.is_empty(), "Empty batch request");
4549
4550 let (version, notifier) = self
4551 .transaction(
4552 move |tx| {
4553 let mut version = version.clone();
4554 let mut notifier = None;
4555 for append_request in &batch {
4556 let (v, n) =
4557 Self::append(tx, &execution_id, append_request.clone(), version)?;
4558 version = v;
4559 notifier = Some(n);
4560 }
4561 Ok::<_, DbErrorWrite>((
4562 version,
4563 notifier.expect("checked that the batch is not empty"),
4564 ))
4565 },
4566 TxType::MultipleWrites,
4567 "append_batch",
4568 )
4569 .await?;
4570
4571 self.notify_all(vec![notifier], current_time);
4572 Ok(version)
4573 }
4574
4575 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %version))]
4576 async fn append_batch_create_new_execution(
4577 &self,
4578 current_time: DateTime<Utc>,
4579 batch: Vec<AppendRequest>,
4580 execution_id: ExecutionId,
4581 version: Version,
4582 child_req: Vec<CreateRequest>,
4583 backtraces: Vec<BacktraceInfo>,
4584 ) -> Result<AppendBatchResponse, DbErrorWrite> {
4585 debug!("append_batch_create_new_execution");
4586 trace!(?batch, ?child_req, "append_batch_create_new_execution");
4587 assert!(!batch.is_empty(), "Empty batch request");
4588
4589 let (version, notifiers) = self
4590 .transaction(
4591 move |tx| {
4592 let mut notifier = None;
4593 let mut version = version.clone();
4594 for append_request in &batch {
4595 let (v, n) =
4596 Self::append(tx, &execution_id, append_request.clone(), version)?;
4597 version = v;
4598 notifier = Some(n);
4599 }
4600 let mut notifiers = Vec::new();
4601 notifiers.push(notifier.expect("checked that the batch is not empty"));
4602
4603 for child_req in &child_req {
4604 let (_, notifier) = Self::create_inner(tx, child_req.clone())?;
4605 notifiers.push(notifier);
4606 }
4607 Ok::<_, DbErrorWrite>((version, notifiers))
4608 },
4609 TxType::MultipleWrites,
4610 "append_batch_create_new_execution_inner",
4611 )
4612 .await?;
4613 self.notify_all(notifiers, current_time);
4614 self.transaction_fire_forget(
4615 move |tx| {
4616 for backtrace in &backtraces {
4617 Self::append_backtrace(tx, backtrace)?;
4618 }
4619 Ok::<_, DbErrorWrite>(())
4620 },
4621 "append_batch_create_new_execution_append_backtrace",
4622 )
4623 .await;
4624 Ok(version)
4625 }
4626
4627 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4631 async fn subscribe_to_next_responses(
4632 &self,
4633 execution_id: &ExecutionId,
4634 last_response: ResponseCursor,
4635 timeout_fut: Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>,
4636 ) -> Result<Vec<ResponseWithCursor>, DbErrorReadWithTimeout> {
4637 debug!("next_responses");
4638 let unique_tag: u64 = rand::random();
4639 let execution_id = execution_id.clone();
4640
4641 let cleanup = || {
4642 let mut guard = self.0.response_subscribers.lock().unwrap();
4643 match guard.remove(&execution_id) {
4644 Some((_, tag)) if tag == unique_tag => {} Some(other) => {
4646 guard.insert(execution_id.clone(), other);
4648 }
4649 None => {} }
4651 };
4652
4653 let response_subscribers = self.0.response_subscribers.clone();
4654 let resp_or_receiver = {
4655 let execution_id = execution_id.clone();
4656 self.transaction(
4657 move |tx| {
4658 let responses = Self::get_responses_after(tx, &execution_id, last_response)?;
4659 if responses.is_empty() {
4660 let (sender, receiver) = oneshot::channel();
4662 response_subscribers
4663 .lock()
4664 .unwrap()
4665 .insert(execution_id.clone(), (sender, unique_tag));
4666 Ok::<_, DbErrorReadWithTimeout>(itertools::Either::Right(receiver))
4667 } else {
4668 Ok(itertools::Either::Left(responses))
4669 }
4670 },
4671 TxType::Other, "subscribe_to_next_responses",
4673 )
4674 .await
4675 }
4676 .inspect_err(|_| {
4677 cleanup();
4678 })?;
4679 match resp_or_receiver {
4680 itertools::Either::Left(resp) => Ok(resp), itertools::Either::Right(receiver) => {
4682 let res = tokio::select! {
4683 resp = receiver => {
4684 match resp {
4685 Ok(resp) => Ok(vec![resp]),
4686 Err(_) => Err(DbErrorReadWithTimeout::from(DbErrorGeneric::Close)),
4687 }
4688 }
4689 outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
4690 };
4691 cleanup();
4692 res
4693 }
4694 }
4695 }
4696
4697 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4699 async fn wait_for_finished_result(
4700 &self,
4701 execution_id: &ExecutionId,
4702 timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
4703 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
4704 let unique_tag: u64 = rand::random();
4705 let execution_id = execution_id.clone();
4706 let execution_finished_subscription = self.0.execution_finished_subscribers.clone();
4707
4708 let cleanup = || {
4709 let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
4710 if let Some(subscribers) = guard.get_mut(&execution_id) {
4711 subscribers.remove(&unique_tag);
4712 }
4713 };
4714
4715 let resp_or_receiver = {
4716 let execution_id = execution_id.clone();
4717 self.transaction(move |tx| {
4718 let pending_state =
4719 Self::get_combined_state(tx, &execution_id)?.execution_with_state.pending_state;
4720 if let PendingState::Finished(finished) = pending_state {
4721 let event =
4722 Self::get_execution_event(tx, &execution_id, finished.version)?;
4723 if let ExecutionRequest::Finished { retval, ..} = event.event {
4724 Ok(itertools::Either::Left(retval))
4725 } else {
4726 error!("Mismatch, expected Finished row: {event:?} based on t_state {finished}");
4727 Err(DbErrorReadWithTimeout::from(consistency_db_err(
4728 "cannot get finished event based on t_state version"
4729 )))
4730 }
4731 } else {
4732 let (sender, receiver) = oneshot::channel();
4736 let mut guard = execution_finished_subscription.lock().unwrap();
4737 guard.entry(execution_id.clone()).or_default().insert(unique_tag, sender);
4738 Ok(itertools::Either::Right(receiver))
4739 }
4740 },
4741 TxType::Other, "wait_for_finished_result")
4743 .await
4744 }
4745 .inspect_err(|_| {
4746 cleanup();
4750 })?;
4751
4752 let timeout_fut = timeout_fut.unwrap_or_else(|| Box::pin(std::future::pending()));
4753 match resp_or_receiver {
4754 itertools::Either::Left(resp) => Ok(resp), itertools::Either::Right(receiver) => {
4756 let res = tokio::select! {
4757 resp = receiver => {
4758 match resp {
4759 Ok(retval) => Ok(retval),
4760 Err(_recv_err) => Err(DbErrorGeneric::Close.into())
4761 }
4762 }
4763 outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
4764 };
4765 cleanup();
4766 res
4767 }
4768 }
4769 }
4770
4771 #[instrument(level = Level::DEBUG, skip_all, fields(%join_set_id, %execution_id))]
4772 async fn append_delay_response(
4773 &self,
4774 created_at: DateTime<Utc>,
4775 execution_id: ExecutionId,
4776 join_set_id: JoinSetId,
4777 delay_id: DelayId,
4778 result: Result<(), ()>,
4779 ) -> Result<AppendDelayResponseOutcome, DbErrorWrite> {
4780 debug!("append_delay_response");
4781 let event = JoinSetResponseEventOuter {
4782 created_at,
4783 event: JoinSetResponseEvent {
4784 join_set_id,
4785 event: JoinSetResponse::DelayFinished {
4786 delay_id: delay_id.clone(),
4787 result,
4788 },
4789 },
4790 };
4791 let res = self
4792 .transaction(
4793 {
4794 let execution_id = execution_id.clone();
4795 move |tx| Self::append_response(tx, &execution_id, event.clone())
4796 },
4797 TxType::MultipleWrites,
4798 "append_delay_response",
4799 )
4800 .await;
4801 match res {
4802 Ok(notifier) => {
4803 self.notify_all(vec![notifier], created_at);
4804 Ok(AppendDelayResponseOutcome::Success)
4805 }
4806 Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)) => {
4807 let delay_success = self
4808 .transaction(
4809 move |tx| Self::get_delay_response(tx, &execution_id, &delay_id),
4810 TxType::Other, "get_delay_response",
4812 )
4813 .await?;
4814 match delay_success {
4815 Some(true) => Ok(AppendDelayResponseOutcome::AlreadyFinished),
4816 Some(false) => Ok(AppendDelayResponseOutcome::AlreadyCancelled),
4817 None => Err(DbErrorWrite::Generic(DbErrorGeneric::Uncategorized {
4818 reason: "insert failed yet select did not find the response".into(),
4819 context: SpanTrace::capture(),
4820 source: None,
4821 loc: Location::caller(),
4822 })),
4823 }
4824 }
4825 Err(err) => Err(err),
4826 }
4827 }
4828
4829 #[instrument(level = Level::DEBUG, skip_all)]
4830 async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite> {
4831 trace!("append_backtrace");
4832 self.transaction_fire_forget(
4833 move |tx| Self::append_backtrace(tx, &append),
4834 "append_backtrace",
4835 )
4836 .await;
4837 Ok(())
4838 }
4839
4840 #[instrument(level = Level::DEBUG, skip_all)]
4841 async fn append_backtrace_batch(&self, batch: Vec<BacktraceInfo>) -> Result<(), DbErrorWrite> {
4842 trace!("append_backtrace_batch");
4843 self.transaction_fire_forget(
4844 move |tx| {
4845 for append in &batch {
4846 Self::append_backtrace(tx, append)?;
4847 }
4848 Ok::<_, DbErrorWrite>(())
4849 },
4850 "append_backtrace_batch",
4851 )
4852 .await;
4853 Ok(())
4854 }
4855
4856 #[instrument(level = Level::DEBUG, skip_all)]
4857 async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite> {
4858 trace!("append_log");
4859 self.transaction_fire_forget(move |tx| Self::append_log(tx, &row), "append_log")
4860 .await;
4861 Ok(())
4862 }
4863
4864 #[instrument(level = Level::DEBUG, skip_all)]
4865 async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite> {
4866 trace!("append_log_batch");
4867 let batch = Vec::from(batch);
4868 self.transaction_fire_forget(
4869 move |tx| {
4870 for row in &batch {
4871 Self::append_log(tx, row)?;
4872 }
4873 Ok::<_, DbErrorWrite>(())
4874 },
4875 "append_log_batch",
4876 )
4877 .await;
4878 Ok(())
4879 }
4880
4881 #[instrument(level = Level::TRACE, skip(self))]
4883 async fn get_expired_timers(
4884 &self,
4885 at: DateTime<Utc>,
4886 ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric> {
4887 self.transaction(
4888 move |conn| {
4889 let mut expired_timers = conn.prepare(
4890 "SELECT execution_id, join_set_id, delay_id FROM t_delay WHERE expires_at <= :at",
4891 )?
4892 .query_map(
4893 named_params! {
4894 ":at": at,
4895 },
4896 |row| {
4897 let execution_id = row.get("execution_id")?;
4898 let join_set_id = row.get::<_, JoinSetId>("join_set_id")?;
4899 let delay_id = row.get::<_, DelayId>("delay_id")?;
4900 let delay = ExpiredDelay { execution_id, join_set_id, delay_id };
4901 Ok(ExpiredTimer::Delay(delay))
4902 },
4903 )?
4904 .collect::<Result<Vec<_>, _>>()?;
4905 let expired = conn.prepare(&format!(r#"
4907 SELECT execution_id, last_lock_version, corresponding_version, intermittent_event_count, max_retries, retry_exp_backoff_millis,
4908 executor_id, run_id
4909 FROM t_state
4910 WHERE pending_expires_finished <= :at AND state = "{STATE_LOCKED}"
4911 "#
4912 )
4913 )?
4914 .query_map(
4915 named_params! {
4916 ":at": at,
4917 },
4918 |row| {
4919 let execution_id = row.get("execution_id")?;
4920 let locked_at_version = Version::new(row.get("last_lock_version")?);
4921 let next_version = Version::new(row.get("corresponding_version")?).increment();
4922 let intermittent_event_count = row.get("intermittent_event_count")?;
4923 let max_retries = row.get("max_retries")?;
4924 let retry_exp_backoff_millis = u64::from(row.get::<_, u32>("retry_exp_backoff_millis")?);
4925 let executor_id = row.get("executor_id")?;
4926 let run_id = row.get("run_id")?;
4927 let lock = ExpiredLock {
4928 execution_id,
4929 locked_at_version,
4930 next_version,
4931 intermittent_event_count,
4932 max_retries,
4933 retry_exp_backoff: Duration::from_millis(retry_exp_backoff_millis),
4934 locked_by: LockedBy { executor_id, run_id },
4935 };
4936 Ok(ExpiredTimer::Lock(lock))
4937 }
4938 )?
4939 .collect::<Result<Vec<_>, _>>()?;
4940 expired_timers.extend(expired);
4941 if !expired_timers.is_empty() {
4942 debug!("get_expired_timers found {expired_timers:?}");
4943 }
4944 Ok(expired_timers)
4945 },
4946 TxType::Other, "get_expired_timers"
4948 )
4949 .await
4950 .map_err(to_generic_error)
4951 }
4952
4953 async fn get_execution_event(
4954 &self,
4955 execution_id: &ExecutionId,
4956 version: &Version,
4957 ) -> Result<ExecutionEvent, DbErrorRead> {
4958 let version = version.0;
4959 let execution_id = execution_id.clone();
4960 self.transaction(
4961 move |tx| Self::get_execution_event(tx, &execution_id, version),
4962 TxType::Other, "get_execution_event",
4964 )
4965 .await
4966 }
4967
4968 async fn get_pending_state(
4969 &self,
4970 execution_id: &ExecutionId,
4971 ) -> Result<ExecutionWithState, DbErrorRead> {
4972 let execution_id = execution_id.clone();
4973 Ok(self
4974 .transaction(
4975 move |tx| Self::get_combined_state(tx, &execution_id),
4976 TxType::Other, "get_pending_state",
4978 )
4979 .await?
4980 .execution_with_state)
4981 }
4982}
4983
4984#[cfg(feature = "test")]
4985#[async_trait]
4986impl concepts::storage::DbConnectionTest for SqlitePool {
4987 #[instrument(level = Level::DEBUG, skip(self, response_event), fields(join_set_id = %response_event.join_set_id))]
4988 async fn append_response(
4989 &self,
4990 created_at: DateTime<Utc>,
4991 execution_id: ExecutionId,
4992 response_event: JoinSetResponseEvent,
4993 ) -> Result<(), DbErrorWrite> {
4994 debug!("append_response");
4995 let event = JoinSetResponseEventOuter {
4996 created_at,
4997 event: response_event,
4998 };
4999 let notifier = self
5000 .transaction(
5001 move |tx| Self::append_response(tx, &execution_id, event.clone()),
5002 TxType::Other, "append_response",
5004 )
5005 .await?;
5006 self.notify_all(vec![notifier], created_at);
5007 Ok(())
5008 }
5009}
5010
5011#[cfg(any(test, feature = "tempfile"))]
5012pub mod tempfile {
5013 use super::{SqliteConfig, SqlitePool};
5014 use tempfile::NamedTempFile;
5015
5016 pub async fn sqlite_pool() -> (SqlitePool, Option<NamedTempFile>) {
5017 if let Ok(path) = std::env::var("SQLITE_FILE") {
5018 (
5019 SqlitePool::new(path, SqliteConfig::default())
5020 .await
5021 .unwrap(),
5022 None,
5023 )
5024 } else {
5025 let file = NamedTempFile::new().unwrap();
5026 let path = file.path();
5027 (
5028 SqlitePool::new(path, SqliteConfig::default())
5029 .await
5030 .unwrap(),
5031 Some(file),
5032 )
5033 }
5034 }
5035}
5036
5037#[cfg(test)]
5038mod tests {
5039 use crate::sqlite_dao::{SqlitePool, TxType, tempfile::sqlite_pool};
5040 use assert_matches::assert_matches;
5041 use chrono::DateTime;
5042 use concepts::{
5043 ComponentId, FunctionFqn, Params,
5044 prefixed_ulid::{DEPLOYMENT_ID_DUMMY, EXECUTION_ID_DUMMY},
5045 storage::{CreateRequest, DbErrorWrite, DbErrorWriteNonRetriable, DbPoolCloseable},
5046 };
5047 use rusqlite::named_params;
5048
5049 const SOME_FFQN: FunctionFqn = FunctionFqn::new_static("ns:pkg/ifc", "fn");
5050
5051 #[tokio::test]
5052 async fn failing_ltx_should_be_rolled_back() -> Result<(), DbErrorWrite> {
5053 let created_at = DateTime::from_timestamp_nanos(0);
5054 let (pool, _guard) = sqlite_pool().await;
5055 pool.transaction(
5056 move |tx| {
5057 let req = CreateRequest {
5058 created_at,
5059 execution_id: EXECUTION_ID_DUMMY,
5060 ffqn: SOME_FFQN,
5061 params: Params::empty(),
5062 parent: None,
5063 metadata: concepts::ExecutionMetadata::empty(),
5064 scheduled_at: created_at,
5065 component_id: ComponentId::dummy_activity(),
5066 deployment_id: DEPLOYMENT_ID_DUMMY,
5067 scheduled_by: None,
5068 };
5069 SqlitePool::create_inner(tx, req)?;
5070 SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at)?;
5071 Ok::<_, DbErrorWrite>(())
5072 },
5073 TxType::MultipleWrites,
5074 "create_inner + pause_execution",
5075 )
5076 .await?;
5077
5078 let err = pool
5080 .transaction(
5081 move |tx| SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at),
5082 TxType::MultipleWrites,
5083 "pause_execution",
5084 )
5085 .await
5086 .unwrap_err();
5087 let reason = assert_matches!(err, DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::IllegalState { reason, .. }) => reason);
5088 assert_eq!("cannot pause, execution is already paused", reason.as_ref());
5089
5090 let events = pool.transaction(
5091 move |tx| {
5092 let events =
5093 tx.prepare(
5094 "SELECT created_at, json_value, version FROM t_execution_log WHERE execution_id = :execution_id",
5095 )?
5096 .query_map(
5097 named_params! {
5098 ":execution_id": EXECUTION_ID_DUMMY.to_string(),
5099 },
5100 SqlitePool::map_t_execution_log_row,
5101 )
5102 .map_err(DbErrorWrite::from)?
5103 .collect::<Result<Vec<_>, _>>()?;
5104
5105 Ok::<_, DbErrorWrite>(events)
5106 },
5107 TxType::Other, "get_log",
5109 )
5110 .await?;
5111 assert_eq!(2, events.len());
5112 pool.close().await;
5113 Ok(())
5114 }
5115}