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, str::FromStr as _};
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 show_derived: bool,
3220 filter: &LogFilter,
3221 pagination: &Pagination<DateTime<Utc>>,
3222 ) -> Result<ListLogsResponse, DbErrorRead> {
3223 let length = pagination.length();
3224 let exec_id_str = execution_id.to_string();
3225 let exec_id_filter = if show_derived {
3226 "LIKE :execution_id || '%'"
3227 } else {
3228 "= :execution_id"
3229 };
3230 let mut query = format!(
3231 "SELECT id, run_id, created_at, level, message, stream_type, payload, execution_id
3232 FROM t_log
3233 WHERE execution_id {exec_id_filter}",
3234 );
3235
3236 let cursor_val = pagination.cursor();
3237 let params = vec![
3238 (":execution_id", &exec_id_str as &dyn rusqlite::ToSql),
3239 (":cursor", cursor_val as &dyn rusqlite::ToSql),
3240 (":length", &length as &dyn rusqlite::ToSql),
3241 ];
3242
3243 let level_filter = if filter.should_show_logs() {
3245 let levels_str = if !filter.levels().is_empty() {
3246 filter
3247 .levels()
3248 .iter()
3249 .map(|lvl| (*lvl as u8).to_string())
3250 .collect::<Vec<_>>()
3251 .join(",")
3252 } else {
3253 LogLevel::iter()
3254 .map(|lvl| (lvl as u8).to_string())
3255 .collect::<Vec<_>>()
3256 .join(",")
3257 };
3258 Some(format!(" level IN ({levels_str})"))
3259 } else {
3260 None
3261 };
3262 let stream_filter = if filter.should_show_streams() {
3263 let streams_str = if !filter.stream_types().is_empty() {
3264 filter
3265 .stream_types()
3266 .iter()
3267 .map(|st| (*st as u8).to_string())
3268 .collect::<Vec<_>>()
3269 .join(",")
3270 } else {
3271 LogStreamType::iter()
3272 .map(|st| (st as u8).to_string())
3273 .collect::<Vec<_>>()
3274 .join(",")
3275 };
3276 Some(format!(" stream_type IN ({streams_str})"))
3277 } else {
3278 None
3279 };
3280 match (level_filter, stream_filter) {
3281 (Some(level_filter), Some(stream_filter)) => {
3282 write!(&mut query, " AND ({level_filter} OR {stream_filter})")
3283 .expect("writing to string");
3284 }
3285 (Some(level_filter), None) => {
3286 write!(&mut query, " AND {level_filter}").expect("writing to string");
3287 }
3288 (None, Some(stream_filter)) => {
3289 write!(&mut query, " AND {stream_filter}").expect("writing to string");
3290 }
3291 (None, None) => unreachable!("guarded by constructor"),
3292 }
3293
3294 write!(&mut query, " AND created_at {} :cursor", pagination.rel())
3296 .expect("writing to string");
3297
3298 query.push_str(" ORDER BY created_at ");
3300 query.push_str(pagination.asc_or_desc());
3301 query.push_str(", id ");
3302 query.push_str(pagination.asc_or_desc());
3303
3304 query.push_str(" LIMIT :length");
3306
3307 let mut stmt = tx.prepare(&query)?;
3308
3309 let items = stmt
3310 .query_map(params.as_slice(), |row| {
3311 let created_at: DateTime<Utc> = row.get("created_at")?;
3312 let run_id = row.get("run_id")?;
3313 let level: Option<u8> = row.get("level")?;
3314 let message: Option<String> = row.get("message")?;
3315 let stream_type: Option<u8> = row.get("stream_type")?;
3316 let payload: Option<Vec<u8>> = row.get("payload")?;
3317 let execution_id_str: String = row.get("execution_id")?;
3318 let execution_id = ExecutionId::from_str(&execution_id_str).map_err(|_| {
3319 consistency_rusqlite(format!("cannot convert ExecutionId {execution_id_str}"))
3320 })?;
3321
3322 let log_entry = match (level, message, stream_type, payload) {
3323 (Some(lvl), Some(msg), None, None) => LogEntry::Log {
3324 created_at,
3325 level: LogLevel::try_from(lvl).map_err(|_| {
3326 consistency_rusqlite(format!("cannot convert {lvl} to LogLevel"))
3327 })?,
3328 message: msg,
3329 },
3330 (None, None, Some(stype), Some(pl)) => LogEntry::Stream {
3331 created_at,
3332 stream_type: LogStreamType::try_from(stype).map_err(|_| {
3333 consistency_rusqlite(format!("cannot convert {stype} to LogStreamType"))
3334 })?,
3335 payload: pl,
3336 },
3337 _ => {
3338 return Err(consistency_rusqlite("invalid t_log row".to_string()));
3339 }
3340 };
3341 Ok(LogEntryRow {
3342 cursor: created_at,
3343 run_id,
3344 log_entry,
3345 execution_id,
3346 })
3347 })?
3348 .collect::<Result<Vec<_>, _>>()?;
3349
3350 Ok(ListLogsResponse {
3351 next_page: items
3352 .last()
3353 .map(|item| Pagination::NewerThan {
3354 length: pagination.length(),
3355 cursor: item.cursor,
3356 including_cursor: false,
3357 })
3358 .unwrap_or({
3359 if pagination.is_asc() {
3360 *pagination } else {
3362 Pagination::NewerThan {
3364 length: pagination.length(),
3365 cursor: DateTime::<Utc>::UNIX_EPOCH,
3366 including_cursor: true,
3367 }
3368 }
3369 }),
3370 prev_page: match items.first() {
3371 Some(item) => Some(Pagination::OlderThan {
3372 length: pagination.length(),
3373 cursor: item.cursor,
3374 including_cursor: false,
3375 }),
3376 None if pagination.is_asc()
3377 && pagination.cursor() > &DateTime::<Utc>::UNIX_EPOCH =>
3378 {
3379 Some(pagination.invert())
3381 }
3382 None => None,
3383 },
3384 items,
3385 })
3386 }
3387
3388 fn list_deployment_states(
3389 tx: &Transaction,
3390 current_time: DateTime<Utc>,
3391 pagination: Pagination<Option<DeploymentId>>,
3392 include_config_json: bool,
3393 ) -> Result<Vec<DeploymentState>, DbErrorRead> {
3394 let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3395 let config_json_col = if include_config_json {
3396 "d.config_json"
3397 } else {
3398 "NULL AS config_json"
3399 };
3400 let mut sql = format!(
3401 r"
3402 SELECT
3403 d.deployment_id,
3404 COALESCE(SUM(s.state = '{STATE_LOCKED}'), 0) AS locked,
3405 COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.pending_expires_finished <= :now), 0) AS pending,
3406 COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.pending_expires_finished > :now), 0) AS scheduled,
3407 COALESCE(SUM(s.state = '{STATE_BLOCKED_BY_JOIN_SET}'), 0) AS blocked,
3408 COALESCE(SUM(s.state = '{STATE_FINISHED}'), 0) AS finished,
3409 {config_json_col},
3410 d.created_at,
3411 d.last_active_at,
3412 d.status
3413 FROM t_deployment d
3414 LEFT JOIN t_state s ON s.deployment_id = d.deployment_id"
3415 );
3416
3417 params.push((":now", Box::new(current_time)));
3418
3419 if let Some(cursor) = pagination.cursor() {
3420 params.push((":cursor", Box::new(*cursor)));
3421 write!(
3422 sql,
3423 " WHERE d.deployment_id {rel} :cursor",
3424 rel = pagination.rel()
3425 )
3426 .expect("writing to string");
3427 }
3428
3429 let (inner_order, outer_order) = if pagination.is_desc() {
3432 ("DESC", "")
3433 } else {
3434 ("ASC", "DESC")
3435 };
3436
3437 write!(
3438 sql,
3439 " 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}",
3440 limit = pagination.length()
3441 )
3442 .expect("writing to string");
3443
3444 let final_sql = if outer_order.is_empty() {
3445 sql
3446 } else {
3447 format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3448 };
3449
3450 let result: Vec<DeploymentState> = tx
3451 .prepare(&final_sql)?
3452 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3453 params
3454 .iter()
3455 .map(|(k, v)| (*k, v.as_ref()))
3456 .collect::<Vec<_>>()
3457 .as_ref(),
3458 |row| {
3459 let status_str: String = row.get("status")?;
3460 let status = status_str.parse::<DeploymentStatus>().map_err(|_| {
3461 rusqlite::Error::InvalidColumnType(
3462 0,
3463 "status".to_string(),
3464 rusqlite::types::Type::Text,
3465 )
3466 })?;
3467 Ok(DeploymentState {
3468 deployment_id: row.get("deployment_id")?,
3469 locked: row.get("locked")?,
3470 pending: row.get("pending")?,
3471 scheduled: row.get("scheduled")?,
3472 blocked: row.get("blocked")?,
3473 finished: row.get("finished")?,
3474 config_json: row.get("config_json")?,
3475 created_at: row.get("created_at")?,
3476 last_active_at: row.get("last_active_at")?,
3477 status,
3478 })
3479 },
3480 )?
3481 .collect::<Result<Vec<_>, rusqlite::Error>>()
3482 .map_err(DbErrorRead::from)?;
3483
3484 Ok(result)
3485 }
3486
3487 fn insert_deployment_tx(
3488 tx: &Transaction,
3489 record: &DeploymentRecord,
3490 ) -> Result<(), DbErrorWrite> {
3491 assert_eq!(
3492 record.status,
3493 DeploymentStatus::Inactive,
3494 "insert_deployment requires Inactive status"
3495 );
3496 assert!(
3497 record.last_active_at.is_none(),
3498 "insert_deployment requires last_active_at == None"
3499 );
3500 tx.execute(
3501 "INSERT INTO t_deployment \
3502 (deployment_id, created_at, status, config_json, obelisk_version, created_by) \
3503 VALUES (:deployment_id, :created_at, :status, :config_json, :obelisk_version, :created_by)",
3504 rusqlite::named_params! {
3505 ":deployment_id": record.deployment_id.to_string(),
3506 ":created_at": record.created_at,
3507 ":status": record.status.as_str(),
3508 ":config_json": record.config_json,
3509 ":obelisk_version": record.obelisk_version,
3510 ":created_by": record.created_by,
3511 },
3512 )
3513 .map_err(RusqliteError::from)?;
3514 Ok(())
3515 }
3516
3517 fn activate_deployment_tx(
3518 tx: &Transaction,
3519 deployment_id: DeploymentId,
3520 now: DateTime<Utc>,
3521 ) -> Result<(), DbErrorWrite> {
3522 tx.execute(
3524 "UPDATE t_deployment SET status = 'inactive' WHERE status IN ('active', 'enqueued')",
3525 [],
3526 )
3527 .map_err(RusqliteError::from)?;
3528 let rows = tx
3530 .execute(
3531 "UPDATE t_deployment SET status = 'active', last_active_at = :now WHERE deployment_id = :deployment_id",
3532 rusqlite::named_params! {
3533 ":now": now,
3534 ":deployment_id": deployment_id.to_string(),
3535 },
3536 )
3537 .map_err(RusqliteError::from)?;
3538 if rows == 0 {
3539 return Err(DbErrorWrite::NotFound);
3540 }
3541 Ok(())
3542 }
3543
3544 fn enqueue_deployment_tx(
3545 tx: &Transaction,
3546 deployment_id: DeploymentId,
3547 ) -> Result<(), DbErrorWrite> {
3548 let status_opt: Option<String> = tx
3550 .query_row(
3551 "SELECT status FROM t_deployment WHERE deployment_id = :deployment_id",
3552 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3553 |row| row.get(0),
3554 )
3555 .optional()
3556 .map_err(RusqliteError::from)?;
3557 match status_opt.as_deref() {
3558 None => return Err(DbErrorWrite::NotFound),
3559 Some("active") => return Err(DbErrorWriteNonRetriable::Conflict.into()),
3560 _ => {}
3561 }
3562 tx.execute(
3564 "UPDATE t_deployment SET status = 'inactive' WHERE status = 'enqueued'",
3565 [],
3566 )
3567 .map_err(RusqliteError::from)?;
3568 let rows = tx
3570 .execute(
3571 "UPDATE t_deployment SET status = 'enqueued' WHERE deployment_id = :deployment_id",
3572 rusqlite::named_params! {
3573 ":deployment_id": deployment_id.to_string(),
3574 },
3575 )
3576 .map_err(RusqliteError::from)?;
3577 if rows == 0 {
3578 return Err(DbErrorWrite::NotFound);
3579 }
3580 Ok(())
3581 }
3582
3583 fn get_deployment_tx(
3584 tx: &Transaction,
3585 deployment_id: DeploymentId,
3586 ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3587 tx.query_row(
3588 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
3589 FROM t_deployment WHERE deployment_id = :deployment_id",
3590 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3591 deployment_record_from_row,
3592 )
3593 .optional()
3594 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))
3595 }
3596
3597 #[cfg(feature = "test")]
3598 fn get_active_deployment_tx(tx: &Transaction) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3599 tx.query_row(
3600 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
3601 FROM t_deployment WHERE status = 'active' LIMIT 1",
3602 [],
3603 deployment_record_from_row,
3604 )
3605 .optional()
3606 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))
3607 }
3608
3609 fn list_deployments_tx(
3610 tx: &Transaction,
3611 pagination: Pagination<Option<DeploymentId>>,
3612 ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
3613 let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3614 let mut sql = String::from(
3615 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
3616 FROM t_deployment",
3617 );
3618
3619 if let Some(cursor) = pagination.cursor() {
3620 params.push((":cursor", Box::new(*cursor)));
3621 write!(
3622 sql,
3623 " WHERE deployment_id {rel} :cursor",
3624 rel = pagination.rel()
3625 )
3626 .expect("writing to string");
3627 }
3628
3629 let (inner_order, outer_order) = if pagination.is_desc() {
3630 ("DESC", "")
3631 } else {
3632 ("ASC", "DESC")
3633 };
3634
3635 write!(
3636 sql,
3637 " ORDER BY deployment_id {inner_order} LIMIT {limit}",
3638 limit = pagination.length()
3639 )
3640 .expect("writing to string");
3641
3642 let final_sql = if outer_order.is_empty() {
3643 sql
3644 } else {
3645 format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3646 };
3647
3648 let result: Vec<DeploymentRecord> = tx
3649 .prepare(&final_sql)?
3650 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3651 params
3652 .iter()
3653 .map(|(k, v)| (*k, v.as_ref()))
3654 .collect::<Vec<_>>()
3655 .as_ref(),
3656 deployment_record_from_row,
3657 )?
3658 .collect::<Result<Vec<_>, rusqlite::Error>>()
3659 .map_err(DbErrorRead::from)?;
3660
3661 Ok(result)
3662 }
3663
3664 fn pause_execution(
3665 tx: &Transaction,
3666 execution_id: &ExecutionId,
3667 paused_at: DateTime<Utc>,
3668 ) -> Result<Version, DbErrorWrite> {
3669 let combined_state = Self::get_combined_state(tx, execution_id)?;
3670 let appending_version = combined_state.get_next_version_fail_if_finished()?;
3671 debug!("Pausing with {appending_version}");
3672 let (next_version, _) = Self::append(
3673 tx,
3674 execution_id,
3675 AppendRequest {
3676 created_at: paused_at,
3677 event: ExecutionRequest::Paused,
3678 },
3679 appending_version,
3680 )?;
3681 Ok(next_version)
3682 }
3683
3684 fn unpause_execution(
3685 tx: &Transaction,
3686 execution_id: &ExecutionId,
3687 paused_at: DateTime<Utc>,
3688 ) -> Result<Version, DbErrorWrite> {
3689 let combined_state = Self::get_combined_state(tx, execution_id)?;
3690 let appending_version = combined_state.get_next_version_fail_if_finished()?;
3691 debug!("Unpausing with {appending_version}");
3692 let (next_version, _) = Self::append(
3693 tx,
3694 execution_id,
3695 AppendRequest {
3696 created_at: paused_at,
3697 event: ExecutionRequest::Unpaused,
3698 },
3699 appending_version,
3700 )?;
3701 Ok(next_version)
3702 }
3703}
3704
3705#[async_trait]
3706impl DbExecutor for SqlitePool {
3707 #[instrument(level = Level::TRACE, skip(self))]
3708 async fn lock_pending_by_ffqns(
3709 &self,
3710 batch_size: u32,
3711 pending_at_or_sooner: DateTime<Utc>,
3712 ffqns: Arc<[FunctionFqn]>,
3713 created_at: DateTime<Utc>,
3714 component_id: ComponentId,
3715 deployment_id: DeploymentId,
3716 executor_id: ExecutorId,
3717 lock_expires_at: DateTime<Utc>,
3718 run_id: RunId,
3719 retry_config: ComponentRetryConfig,
3720 ) -> Result<LockPendingResponse, DbErrorWrite> {
3721 let execution_ids_versions = self
3722 .transaction(
3723 move |conn| {
3724 Self::get_pending_by_ffqns(conn, batch_size, pending_at_or_sooner, &ffqns)
3725 },
3726 TxType::Other, "lock_pending_by_ffqns_get",
3728 )
3729 .await
3730 .map_err(to_generic_error)?;
3731 if execution_ids_versions.is_empty() {
3732 Ok(vec![])
3733 } else {
3734 debug!("Locking {execution_ids_versions:?}");
3735 self.transaction(
3736 move |tx| {
3737 let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
3738 for (execution_id, version) in &execution_ids_versions {
3740 locked_execs.push(Self::lock_single_execution(
3741 tx,
3742 created_at,
3743 &component_id,
3744 deployment_id,
3745 execution_id,
3746 run_id,
3747 version,
3748 executor_id,
3749 lock_expires_at,
3750 retry_config,
3751 )?);
3752 }
3753 Ok::<_, DbErrorWrite>(locked_execs)
3754 },
3755 TxType::MultipleWrites,
3756 "lock_pending_by_ffqns_one",
3757 )
3758 .await
3759 }
3760 }
3761
3762 #[instrument(level = Level::TRACE, skip(self))]
3763 async fn lock_pending_by_component_digest(
3764 &self,
3765 batch_size: u32,
3766 pending_at_or_sooner: DateTime<Utc>,
3767 component_id: &ComponentId,
3768 deployment_id: DeploymentId,
3769 created_at: DateTime<Utc>,
3770 executor_id: ExecutorId,
3771 lock_expires_at: DateTime<Utc>,
3772 run_id: RunId,
3773 retry_config: ComponentRetryConfig,
3774 ) -> Result<LockPendingResponse, DbErrorWrite> {
3775 let component_id = component_id.clone();
3776 let execution_ids_versions = self
3777 .transaction(
3778 {
3779 let component_id = component_id.clone();
3780 move |conn| {
3781 Self::get_pending_by_component_input_digest(
3782 conn,
3783 batch_size,
3784 pending_at_or_sooner,
3785 &component_id.component_digest,
3786 )
3787 }
3788 },
3789 TxType::Other, "lock_pending_by_component_id_get",
3791 )
3792 .await
3793 .map_err(to_generic_error)?;
3794 if execution_ids_versions.is_empty() {
3795 Ok(vec![])
3796 } else {
3797 debug!("Locking {execution_ids_versions:?}");
3798 self.transaction(
3799 move |tx| {
3800 let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
3801 for (execution_id, version) in &execution_ids_versions {
3803 locked_execs.push(Self::lock_single_execution(
3804 tx,
3805 created_at,
3806 &component_id,
3807 deployment_id,
3808 execution_id,
3809 run_id,
3810 version,
3811 executor_id,
3812 lock_expires_at,
3813 retry_config,
3814 )?);
3815 }
3816 Ok::<_, DbErrorWrite>(locked_execs)
3817 },
3818 TxType::MultipleWrites,
3819 "lock_pending_by_component_id_one",
3820 )
3821 .await
3822 }
3823 }
3824
3825 #[cfg(feature = "test")]
3826 #[instrument(level = Level::DEBUG, skip(self))]
3827 async fn lock_one(
3828 &self,
3829 created_at: DateTime<Utc>,
3830 component_id: ComponentId,
3831 deployment_id: DeploymentId,
3832 execution_id: &ExecutionId,
3833 run_id: RunId,
3834 version: Version,
3835 executor_id: ExecutorId,
3836 lock_expires_at: DateTime<Utc>,
3837 retry_config: ComponentRetryConfig,
3838 ) -> Result<LockedExecution, DbErrorWrite> {
3839 debug!(%execution_id, "lock_one");
3840 let execution_id = execution_id.clone();
3841 self.transaction(
3842 move |tx| {
3843 Self::lock_single_execution(
3844 tx,
3845 created_at,
3846 &component_id,
3847 deployment_id,
3848 &execution_id,
3849 run_id,
3850 &version,
3851 executor_id,
3852 lock_expires_at,
3853 retry_config,
3854 )
3855 },
3856 TxType::MultipleWrites, "lock_inner",
3858 )
3859 .await
3860 }
3861
3862 #[instrument(level = Level::DEBUG, skip(self, req))]
3863 async fn append(
3864 &self,
3865 execution_id: ExecutionId,
3866 version: Version,
3867 req: AppendRequest,
3868 ) -> Result<AppendResponse, DbErrorWrite> {
3869 debug!(%req, "append");
3870 trace!(?req, "append");
3871 let created_at = req.created_at;
3872 let (version, notifier) = self
3873 .transaction(
3874 move |tx| Self::append(tx, &execution_id, req.clone(), version.clone()),
3875 TxType::MultipleWrites, "append",
3877 )
3878 .await?;
3879 self.notify_all(vec![notifier], created_at);
3880 Ok(version)
3881 }
3882
3883 #[instrument(level = Level::DEBUG, skip_all)]
3884 async fn append_batch_respond_to_parent(
3885 &self,
3886 events: AppendEventsToExecution,
3887 response: AppendResponseToExecution,
3888 current_time: DateTime<Utc>,
3889 ) -> Result<AppendBatchResponse, DbErrorWrite> {
3890 debug!("append_batch_respond_to_parent");
3891 if events.execution_id == response.parent_execution_id {
3892 return Err(DbErrorWrite::NonRetriable(
3895 DbErrorWriteNonRetriable::ValidationFailed(
3896 "Parameters `execution_id` and `parent_execution_id` cannot be the same".into(),
3897 ),
3898 ));
3899 }
3900 if events.batch.is_empty() {
3901 error!("Batch cannot be empty");
3902 return Err(DbErrorWrite::NonRetriable(
3903 DbErrorWriteNonRetriable::ValidationFailed("batch cannot be empty".into()),
3904 ));
3905 }
3906 let (version, notifiers) = {
3907 self.transaction(
3908 move |tx| {
3909 let mut version = events.version.clone();
3910 let mut notifier_of_child = None;
3911 for append_request in &events.batch {
3912 let (v, n) = Self::append(
3913 tx,
3914 &events.execution_id,
3915 append_request.clone(),
3916 version,
3917 )?;
3918 version = v;
3919 notifier_of_child = Some(n);
3920 }
3921
3922 let pending_at_parent = Self::append_response(
3923 tx,
3924 &response.parent_execution_id,
3925 JoinSetResponseEventOuter {
3926 created_at: response.created_at,
3927 event: JoinSetResponseEvent {
3928 join_set_id: response.join_set_id.clone(),
3929 event: JoinSetResponse::ChildExecutionFinished {
3930 child_execution_id: response.child_execution_id.clone(),
3931 finished_version: response.finished_version.clone(),
3932 result: response.result.clone(),
3933 },
3934 },
3935 },
3936 )?;
3937 Ok::<_, DbErrorWrite>((
3938 version,
3939 vec![
3940 notifier_of_child.expect("checked that the batch is not empty"),
3941 pending_at_parent,
3942 ],
3943 ))
3944 },
3945 TxType::MultipleWrites,
3946 "append_batch_respond_to_parent",
3947 )
3948 .await?
3949 };
3950 self.notify_all(notifiers, current_time);
3951 Ok(version)
3952 }
3953
3954 #[instrument(level = Level::TRACE, skip(self, timeout_fut))]
3957 async fn wait_for_pending_by_ffqn(
3958 &self,
3959 pending_at_or_sooner: DateTime<Utc>,
3960 ffqns: Arc<[FunctionFqn]>,
3961 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
3962 ) {
3963 let unique_tag: u64 = rand::random();
3964 let (sender, mut receiver) = mpsc::channel(1); {
3966 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
3967 for ffqn in ffqns.as_ref() {
3968 pending_subscribers.insert_ffqn(ffqn.clone(), (sender.clone(), unique_tag));
3969 }
3970 }
3971 async {
3972 let Ok(execution_ids_versions) = self
3973 .transaction(
3974 {
3975 let ffqns = ffqns.clone();
3976 move |conn| Self::get_pending_by_ffqns(conn, 1, pending_at_or_sooner, ffqns.as_ref())
3977 },
3978 TxType::Other, "get_pending_by_ffqns",
3980 )
3981 .await
3982 else {
3983 trace!(
3984 "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
3985 );
3986 timeout_fut.await;
3987 return;
3988 };
3989 if !execution_ids_versions.is_empty() {
3990 trace!("Not waiting, database already contains new pending executions");
3991 return;
3992 }
3993 tokio::select! { _ = receiver.recv() => {
3995 trace!("Received a notification");
3996 }
3997 () = timeout_fut => {
3998 }
3999 }
4000 }.await;
4001 {
4003 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4004 for ffqn in ffqns.as_ref() {
4005 match pending_subscribers.remove_ffqn(ffqn) {
4006 Some((_, tag)) if tag == unique_tag => {
4007 }
4009 Some(other) => {
4010 pending_subscribers.insert_ffqn(ffqn.clone(), other);
4012 }
4013 None => {
4014 }
4016 }
4017 }
4018 }
4019 }
4020
4021 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4024 async fn wait_for_pending_by_component_digest(
4025 &self,
4026 pending_at_or_sooner: DateTime<Utc>,
4027 component_digest: &ComponentDigest,
4028 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4029 ) {
4030 let unique_tag: u64 = rand::random();
4031 let (sender, mut receiver) = mpsc::channel(1); {
4033 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4034 pending_subscribers
4035 .insert_by_component(component_digest.clone(), (sender.clone(), unique_tag));
4036 }
4037 async {
4038 let Ok(execution_ids_versions) = self
4039 .transaction(
4040 {
4041 let input_digest = component_digest.clone();
4042 move |conn| Self::get_pending_by_component_input_digest(conn, 1, pending_at_or_sooner, &input_digest)
4043 },
4044 TxType::Other, "get_pending_by_component_input_digest",
4046 )
4047 .await
4048 else {
4049 trace!(
4050 "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4051 );
4052 timeout_fut.await;
4053 return;
4054 };
4055 if !execution_ids_versions.is_empty() {
4056 trace!("Not waiting, database already contains new pending executions");
4057 return;
4058 }
4059 tokio::select! { _ = receiver.recv() => {
4061 trace!("Received a notification");
4062 }
4063 () = timeout_fut => {
4064 }
4065 }
4066 }.await;
4067 {
4069 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4070
4071 match pending_subscribers.remove_by_component(component_digest) {
4072 Some((_, tag)) if tag == unique_tag => {
4073 }
4075 Some(other) => {
4076 pending_subscribers.insert_by_component(component_digest.clone(), other);
4078 }
4079 None => {
4080 }
4082 }
4083 }
4084 }
4085
4086 async fn get_last_execution_event(
4087 &self,
4088 execution_id: &ExecutionId,
4089 ) -> Result<ExecutionEvent, DbErrorRead> {
4090 let execution_id = execution_id.clone();
4091 self.transaction(
4092 move |tx| Self::get_last_execution_event(tx, &execution_id),
4093 TxType::Other, "get_last_execution_event",
4095 )
4096 .await
4097 }
4098}
4099
4100#[async_trait]
4101impl DbExternalApi for SqlitePool {
4102 #[instrument(skip(self))]
4103 async fn get_backtrace(
4104 &self,
4105 execution_id: &ExecutionId,
4106 filter: BacktraceFilter,
4107 ) -> Result<BacktraceInfo, DbErrorRead> {
4108 debug!("get_backtrace");
4109 let execution_id = execution_id.clone();
4110
4111 self.transaction(
4112 move |tx| {
4113 let select = "SELECT component_id, version_min_including, version_max_excluding, wasm_backtrace FROM t_execution_backtrace e \
4114 INNER JOIN t_wasm_backtrace w ON e.backtrace_hash = w.backtrace_hash \
4115 WHERE execution_id = :execution_id";
4116 let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![(":execution_id", Box::new(execution_id.to_string()))];
4117 let select = match &filter {
4118 BacktraceFilter::Specific(version) =>{
4119 params.push((":version", Box::new(version.0)));
4120 format!("{select} AND version_min_including <= :version AND version_max_excluding > :version")
4121 },
4122 BacktraceFilter::First => format!("{select} ORDER BY version_min_including LIMIT 1"),
4123 BacktraceFilter::Last => format!("{select} ORDER BY version_min_including DESC LIMIT 1")
4124 };
4125 tx
4126 .prepare(&select)
4127 ?
4128 .query_row::<_, &[(&'static str, &dyn ToSql)], _>(
4129 params
4130 .iter()
4131 .map(|(key, value)| (*key, value.as_ref()))
4132 .collect::<Vec<_>>()
4133 .as_ref(),
4134 |row| {
4135 Ok(BacktraceInfo {
4136 execution_id: execution_id.clone(),
4137 component_id: row.get::<_, JsonWrapper<_> >("component_id")?.0,
4138 version_min_including: Version::new(row.get::<_, VersionType>("version_min_including")?),
4139 version_max_excluding: Version::new(row.get::<_, VersionType>("version_max_excluding")?),
4140 wasm_backtrace: row.get::<_, JsonWrapper<_>>("wasm_backtrace")?.0,
4141 })
4142 },
4143 ).map_err(DbErrorRead::from)
4144 },
4145 TxType::Other, "get_last_backtrace",
4147 ).await
4148 }
4149
4150 #[instrument(skip_all)]
4151 async fn upsert_source_file(
4152 &self,
4153 component_digest: &ComponentDigest,
4154 frame_key: &str,
4155 is_suffix: bool,
4156 content: &str,
4157 ) -> Result<(), DbErrorWrite> {
4158 let content_hash: [u8; 32] = Sha256::digest(content.as_bytes()).into();
4159 let component_digest = component_digest.clone();
4160 let frame_key = frame_key.to_owned();
4161 let content = content.to_owned();
4162 self.transaction(
4163 move |tx| {
4164 tx.prepare(
4165 "INSERT OR IGNORE INTO t_source_file (content_hash, content) \
4166 VALUES (:content_hash, :content)",
4167 )?
4168 .execute(named_params! {
4169 ":content_hash": content_hash,
4170 ":content": content,
4171 })?;
4172 tx.prepare(
4173 "INSERT OR IGNORE INTO t_component_source \
4174 (component_digest, frame_key, is_suffix, content_hash) \
4175 VALUES (:component_digest, :frame_key, :is_suffix, :content_hash)",
4176 )?
4177 .execute(named_params! {
4178 ":component_digest": component_digest,
4179 ":frame_key": frame_key,
4180 ":is_suffix": is_suffix,
4181 ":content_hash": content_hash,
4182 })?;
4183 Ok(())
4184 },
4185 TxType::Other,
4186 "upsert_source_file",
4187 )
4188 .await
4189 }
4190
4191 #[instrument(skip_all)]
4192 async fn get_source_file(
4193 &self,
4194 component_digest: &ComponentDigest,
4195 file: &str,
4196 ) -> Result<Option<String>, DbErrorRead> {
4197 let component_digest = component_digest.clone();
4198 let file = file.to_owned();
4199 self.transaction(
4200 move |tx| {
4201 let mut stmt = tx.prepare(
4202 "SELECT s.content \
4203 FROM t_component_source cs \
4204 JOIN t_source_file s ON cs.content_hash = s.content_hash \
4205 WHERE cs.component_digest = :component_digest \
4206 AND ( \
4207 (cs.is_suffix = 0 AND cs.frame_key = :file) \
4208 OR (cs.is_suffix = 1 AND \
4209 substr(:file, length(:file) - length(cs.frame_key) + 1) = cs.frame_key) \
4210 )",
4211 )?;
4212 let rows: Vec<String> = stmt
4213 .query_map(
4214 named_params! {
4215 ":component_digest": component_digest,
4216 ":file": file,
4217 },
4218 |row| row.get(0),
4219 )?
4220 .collect::<Result<_, _>>()?;
4221 match rows.len() {
4222 0 => Ok(None),
4223 1 => Ok(Some(rows.into_iter().next().unwrap())),
4224 _ => {
4225 warn!("Multiple suffix matches for '{file}', returning None");
4226 Ok(None)
4227 }
4228 }
4229 },
4230 TxType::Other,
4231 "get_source_file",
4232 )
4233 .await
4234 }
4235
4236 #[instrument(skip(self))]
4237 async fn list_executions(
4238 &self,
4239 filter: ListExecutionsFilter,
4240 pagination: ExecutionListPagination,
4241 ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric> {
4242 self.transaction(
4243 move |tx| Self::list_executions(tx, &filter, &pagination),
4244 TxType::Other, "list_executions",
4246 )
4247 .await
4248 .map_err(to_generic_error)
4249 }
4250
4251 #[instrument(skip(self))]
4252 async fn list_execution_events(
4253 &self,
4254 execution_id: &ExecutionId,
4255 pagination: Pagination<VersionType>,
4256 include_backtrace_id: bool,
4257 ) -> Result<ListExecutionEventsResponse, DbErrorRead> {
4258 let execution_id = execution_id.clone();
4259 self.transaction(
4260 move |tx| {
4261 let events = Self::list_execution_events(
4262 tx,
4263 &execution_id,
4264 pagination,
4265 include_backtrace_id,
4266 )?;
4267 let max_version = Self::get_max_version(tx, &execution_id)?;
4268 Ok(ListExecutionEventsResponse {
4269 events,
4270 max_version,
4271 })
4272 },
4273 TxType::Other, "get",
4275 )
4276 .await
4277 }
4278
4279 #[instrument(skip(self))]
4280 async fn list_responses(
4281 &self,
4282 execution_id: &ExecutionId,
4283 pagination: Pagination<u32>,
4284 ) -> Result<ListResponsesResponse, DbErrorRead> {
4285 let execution_id = execution_id.clone();
4286 self.transaction(
4287 move |tx| {
4288 let responses = Self::list_responses(tx, &execution_id, Some(pagination))?;
4289 let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4290 Ok(ListResponsesResponse {
4291 responses,
4292 max_cursor,
4293 })
4294 },
4295 TxType::Other, "list_responses",
4297 )
4298 .await
4299 }
4300
4301 #[instrument(skip(self))]
4302 async fn list_execution_events_responses(
4303 &self,
4304 execution_id: &ExecutionId,
4305 req_since: &Version,
4306 req_max_length: VersionType,
4307 req_include_backtrace_id: bool,
4308 resp_pagination: Pagination<u32>,
4309 ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead> {
4310 let execution_id = execution_id.clone();
4311 let req_since = req_since.0;
4312 self.transaction(
4313 move |tx| {
4314 let combined_state = Self::get_combined_state(tx, &execution_id)?;
4315 let events = Self::list_execution_events(
4316 tx,
4317 &execution_id,
4318 Pagination::NewerThan {
4319 length: req_max_length
4320 .try_into()
4321 .expect("req_max_length fits in u16"),
4322 cursor: req_since,
4323 including_cursor: true,
4324 },
4325 req_include_backtrace_id,
4326 )?;
4327 let responses = Self::list_responses(tx, &execution_id, Some(resp_pagination))?;
4328 let max_version = Self::get_max_version(tx, &execution_id)?;
4329 let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4330 Ok(ExecutionWithStateRequestsResponses {
4331 execution_with_state: combined_state.execution_with_state,
4332 events,
4333 responses,
4334 max_version,
4335 max_cursor,
4336 })
4337 },
4338 TxType::Other, "list_execution_events_responses",
4340 )
4341 .await
4342 }
4343
4344 #[instrument(skip(self))]
4345 async fn upgrade_execution_component(
4346 &self,
4347 execution_id: &ExecutionId,
4348 old: &ComponentDigest,
4349 new: &ComponentDigest,
4350 ) -> Result<(), DbErrorWrite> {
4351 let execution_id = execution_id.clone();
4352 let old = old.clone();
4353 let new = new.clone();
4354 self.transaction(
4355 move |tx| Self::upgrade_execution_component_single_write(tx, &execution_id, &old, &new),
4356 TxType::Other, "upgrade_execution_component",
4358 )
4359 .await
4360 }
4361
4362 #[instrument(skip(self))]
4363 async fn list_logs(
4364 &self,
4365 execution_id: &ExecutionId,
4366 show_derived: bool,
4367 filter: LogFilter,
4368 pagination: Pagination<DateTime<Utc>>,
4369 ) -> Result<ListLogsResponse, DbErrorRead> {
4370 let execution_id = execution_id.clone();
4371 self.transaction(
4372 move |tx| Self::list_logs_tx(tx, &execution_id, show_derived, &filter, &pagination),
4373 TxType::Other, "list_logs",
4375 )
4376 .await
4377 }
4378
4379 #[instrument(skip(self))]
4380 async fn list_deployment_states(
4381 &self,
4382 current_time: DateTime<Utc>,
4383 pagination: Pagination<Option<DeploymentId>>,
4384 include_config_json: bool,
4385 ) -> Result<Vec<DeploymentState>, DbErrorRead> {
4386 self.transaction(
4387 move |tx| {
4388 Self::list_deployment_states(tx, current_time, pagination, include_config_json)
4389 },
4390 TxType::Other, "list_deployment_states",
4392 )
4393 .await
4394 }
4395
4396 #[instrument(skip(self))]
4397 async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite> {
4398 self.transaction(
4399 move |tx| Self::insert_deployment_tx(tx, &record),
4400 TxType::MultipleWrites,
4401 "insert_deployment",
4402 )
4403 .await
4404 }
4405
4406 #[instrument(skip(self))]
4407 async fn activate_deployment(
4408 &self,
4409 deployment_id: DeploymentId,
4410 now: DateTime<Utc>,
4411 ) -> Result<(), DbErrorWrite> {
4412 self.transaction(
4413 move |tx| Self::activate_deployment_tx(tx, deployment_id, now),
4414 TxType::MultipleWrites,
4415 "activate_deployment",
4416 )
4417 .await
4418 }
4419
4420 async fn enqueue_deployment(&self, deployment_id: DeploymentId) -> Result<(), DbErrorWrite> {
4421 self.transaction(
4422 move |tx| Self::enqueue_deployment_tx(tx, deployment_id),
4423 TxType::MultipleWrites,
4424 "enqueue_deployment",
4425 )
4426 .await
4427 }
4428
4429 #[instrument(skip(self))]
4430 async fn get_deployment(
4431 &self,
4432 deployment_id: DeploymentId,
4433 ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
4434 self.transaction(
4435 move |tx| Self::get_deployment_tx(tx, deployment_id),
4436 TxType::Other,
4437 "get_deployment",
4438 )
4439 .await
4440 }
4441
4442 #[cfg(feature = "test")]
4443 #[instrument(skip(self))]
4444 async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
4445 self.transaction(
4446 move |tx| Self::get_active_deployment_tx(tx),
4447 TxType::Other,
4448 "get_active_deployment",
4449 )
4450 .await
4451 }
4452
4453 #[instrument(skip(self))]
4454 async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
4455 self.transaction(
4456 move |tx| {
4457 tx.query_row(
4458 "SELECT deployment_id, created_at, last_active_at, status, config_json, obelisk_version, created_by \
4459 FROM t_deployment WHERE status IN ('enqueued', 'active') \
4460 ORDER BY CASE status WHEN 'enqueued' THEN 0 ELSE 1 END LIMIT 1",
4461 [],
4462 deployment_record_from_row,
4463 )
4464 .optional()
4465 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))
4466 },
4467 TxType::Other,
4468 "get_current_deployment",
4469 )
4470 .await
4471 }
4472
4473 #[instrument(skip(self))]
4474 async fn list_deployments(
4475 &self,
4476 pagination: Pagination<Option<DeploymentId>>,
4477 ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
4478 self.transaction(
4479 move |tx| Self::list_deployments_tx(tx, pagination),
4480 TxType::Other,
4481 "list_deployments",
4482 )
4483 .await
4484 }
4485
4486 #[instrument(skip(self))]
4487 async fn pause_execution(
4488 &self,
4489 execution_id: &ExecutionId,
4490 paused_at: DateTime<Utc>,
4491 ) -> Result<AppendResponse, DbErrorWrite> {
4492 let execution_id = execution_id.clone();
4493 self.transaction(
4494 move |tx| SqlitePool::pause_execution(tx, &execution_id, paused_at),
4495 TxType::MultipleWrites,
4496 "pause_execution",
4497 )
4498 .await
4499 }
4500
4501 #[instrument(skip(self))]
4502 async fn unpause_execution(
4503 &self,
4504 execution_id: &ExecutionId,
4505 unpaused_at: DateTime<Utc>,
4506 ) -> Result<AppendResponse, DbErrorWrite> {
4507 let execution_id = execution_id.clone();
4508 self.transaction(
4509 move |tx| SqlitePool::unpause_execution(tx, &execution_id, unpaused_at),
4510 TxType::MultipleWrites,
4511 "unpause_execution",
4512 )
4513 .await
4514 }
4515}
4516
4517#[async_trait]
4518impl DbConnection for SqlitePool {
4519 #[instrument(level = Level::DEBUG, skip_all, fields(execution_id = %req.execution_id))]
4520 async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite> {
4521 debug!("create");
4522 trace!(?req, "create");
4523 let created_at = req.created_at;
4524 let (version, notifier) = self
4525 .transaction(
4526 move |tx| Self::create_inner(tx, req.clone()),
4527 TxType::MultipleWrites,
4528 "create",
4529 )
4530 .await?;
4531 self.notify_all(vec![notifier], created_at);
4532 Ok(version)
4533 }
4534
4535 #[instrument(level = Level::DEBUG, skip(self))]
4536 async fn get(
4537 &self,
4538 execution_id: &ExecutionId,
4539 ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
4540 trace!("get");
4541 let execution_id = execution_id.clone();
4542 self.transaction(
4543 move |tx| Self::get(tx, &execution_id),
4544 TxType::Other, "get",
4546 )
4547 .await
4548 }
4549
4550 #[instrument(level = Level::DEBUG, skip(self, batch))]
4551 async fn append_batch(
4552 &self,
4553 current_time: DateTime<Utc>,
4554 batch: Vec<AppendRequest>,
4555 execution_id: ExecutionId,
4556 version: Version,
4557 ) -> Result<AppendBatchResponse, DbErrorWrite> {
4558 debug!("append_batch");
4559 trace!(?batch, "append_batch");
4560 assert!(!batch.is_empty(), "Empty batch request");
4561
4562 let (version, notifier) = self
4563 .transaction(
4564 move |tx| {
4565 let mut version = version.clone();
4566 let mut notifier = None;
4567 for append_request in &batch {
4568 let (v, n) =
4569 Self::append(tx, &execution_id, append_request.clone(), version)?;
4570 version = v;
4571 notifier = Some(n);
4572 }
4573 Ok::<_, DbErrorWrite>((
4574 version,
4575 notifier.expect("checked that the batch is not empty"),
4576 ))
4577 },
4578 TxType::MultipleWrites,
4579 "append_batch",
4580 )
4581 .await?;
4582
4583 self.notify_all(vec![notifier], current_time);
4584 Ok(version)
4585 }
4586
4587 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %version))]
4588 async fn append_batch_create_new_execution(
4589 &self,
4590 current_time: DateTime<Utc>,
4591 batch: Vec<AppendRequest>,
4592 execution_id: ExecutionId,
4593 version: Version,
4594 child_req: Vec<CreateRequest>,
4595 backtraces: Vec<BacktraceInfo>,
4596 ) -> Result<AppendBatchResponse, DbErrorWrite> {
4597 debug!("append_batch_create_new_execution");
4598 trace!(?batch, ?child_req, "append_batch_create_new_execution");
4599 assert!(!batch.is_empty(), "Empty batch request");
4600
4601 let (version, notifiers) = self
4602 .transaction(
4603 move |tx| {
4604 let mut notifier = None;
4605 let mut version = version.clone();
4606 for append_request in &batch {
4607 let (v, n) =
4608 Self::append(tx, &execution_id, append_request.clone(), version)?;
4609 version = v;
4610 notifier = Some(n);
4611 }
4612 let mut notifiers = Vec::new();
4613 notifiers.push(notifier.expect("checked that the batch is not empty"));
4614
4615 for child_req in &child_req {
4616 let (_, notifier) = Self::create_inner(tx, child_req.clone())?;
4617 notifiers.push(notifier);
4618 }
4619 Ok::<_, DbErrorWrite>((version, notifiers))
4620 },
4621 TxType::MultipleWrites,
4622 "append_batch_create_new_execution_inner",
4623 )
4624 .await?;
4625 self.notify_all(notifiers, current_time);
4626 self.transaction_fire_forget(
4627 move |tx| {
4628 for backtrace in &backtraces {
4629 Self::append_backtrace(tx, backtrace)?;
4630 }
4631 Ok::<_, DbErrorWrite>(())
4632 },
4633 "append_batch_create_new_execution_append_backtrace",
4634 )
4635 .await;
4636 Ok(version)
4637 }
4638
4639 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4643 async fn subscribe_to_next_responses(
4644 &self,
4645 execution_id: &ExecutionId,
4646 last_response: ResponseCursor,
4647 timeout_fut: Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>,
4648 ) -> Result<Vec<ResponseWithCursor>, DbErrorReadWithTimeout> {
4649 debug!("next_responses");
4650 let unique_tag: u64 = rand::random();
4651 let execution_id = execution_id.clone();
4652
4653 let cleanup = || {
4654 let mut guard = self.0.response_subscribers.lock().unwrap();
4655 match guard.remove(&execution_id) {
4656 Some((_, tag)) if tag == unique_tag => {} Some(other) => {
4658 guard.insert(execution_id.clone(), other);
4660 }
4661 None => {} }
4663 };
4664
4665 let response_subscribers = self.0.response_subscribers.clone();
4666 let resp_or_receiver = {
4667 let execution_id = execution_id.clone();
4668 self.transaction(
4669 move |tx| {
4670 let responses = Self::get_responses_after(tx, &execution_id, last_response)?;
4671 if responses.is_empty() {
4672 let (sender, receiver) = oneshot::channel();
4674 response_subscribers
4675 .lock()
4676 .unwrap()
4677 .insert(execution_id.clone(), (sender, unique_tag));
4678 Ok::<_, DbErrorReadWithTimeout>(itertools::Either::Right(receiver))
4679 } else {
4680 Ok(itertools::Either::Left(responses))
4681 }
4682 },
4683 TxType::Other, "subscribe_to_next_responses",
4685 )
4686 .await
4687 }
4688 .inspect_err(|_| {
4689 cleanup();
4690 })?;
4691 match resp_or_receiver {
4692 itertools::Either::Left(resp) => Ok(resp), itertools::Either::Right(receiver) => {
4694 let res = tokio::select! {
4695 resp = receiver => {
4696 match resp {
4697 Ok(resp) => Ok(vec![resp]),
4698 Err(_) => Err(DbErrorReadWithTimeout::from(DbErrorGeneric::Close)),
4699 }
4700 }
4701 outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
4702 };
4703 cleanup();
4704 res
4705 }
4706 }
4707 }
4708
4709 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4711 async fn wait_for_finished_result(
4712 &self,
4713 execution_id: &ExecutionId,
4714 timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
4715 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
4716 let unique_tag: u64 = rand::random();
4717 let execution_id = execution_id.clone();
4718 let execution_finished_subscription = self.0.execution_finished_subscribers.clone();
4719
4720 let cleanup = || {
4721 let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
4722 if let Some(subscribers) = guard.get_mut(&execution_id) {
4723 subscribers.remove(&unique_tag);
4724 }
4725 };
4726
4727 let resp_or_receiver = {
4728 let execution_id = execution_id.clone();
4729 self.transaction(move |tx| {
4730 let pending_state =
4731 Self::get_combined_state(tx, &execution_id)?.execution_with_state.pending_state;
4732 if let PendingState::Finished(finished) = pending_state {
4733 let event =
4734 Self::get_execution_event(tx, &execution_id, finished.version)?;
4735 if let ExecutionRequest::Finished { retval, ..} = event.event {
4736 Ok(itertools::Either::Left(retval))
4737 } else {
4738 error!("Mismatch, expected Finished row: {event:?} based on t_state {finished}");
4739 Err(DbErrorReadWithTimeout::from(consistency_db_err(
4740 "cannot get finished event based on t_state version"
4741 )))
4742 }
4743 } else {
4744 let (sender, receiver) = oneshot::channel();
4748 let mut guard = execution_finished_subscription.lock().unwrap();
4749 guard.entry(execution_id.clone()).or_default().insert(unique_tag, sender);
4750 Ok(itertools::Either::Right(receiver))
4751 }
4752 },
4753 TxType::Other, "wait_for_finished_result")
4755 .await
4756 }
4757 .inspect_err(|_| {
4758 cleanup();
4762 })?;
4763
4764 let timeout_fut = timeout_fut.unwrap_or_else(|| Box::pin(std::future::pending()));
4765 match resp_or_receiver {
4766 itertools::Either::Left(resp) => Ok(resp), itertools::Either::Right(receiver) => {
4768 let res = tokio::select! {
4769 resp = receiver => {
4770 match resp {
4771 Ok(retval) => Ok(retval),
4772 Err(_recv_err) => Err(DbErrorGeneric::Close.into())
4773 }
4774 }
4775 outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
4776 };
4777 cleanup();
4778 res
4779 }
4780 }
4781 }
4782
4783 #[instrument(level = Level::DEBUG, skip_all, fields(%join_set_id, %execution_id))]
4784 async fn append_delay_response(
4785 &self,
4786 created_at: DateTime<Utc>,
4787 execution_id: ExecutionId,
4788 join_set_id: JoinSetId,
4789 delay_id: DelayId,
4790 result: Result<(), ()>,
4791 ) -> Result<AppendDelayResponseOutcome, DbErrorWrite> {
4792 debug!("append_delay_response");
4793 let event = JoinSetResponseEventOuter {
4794 created_at,
4795 event: JoinSetResponseEvent {
4796 join_set_id,
4797 event: JoinSetResponse::DelayFinished {
4798 delay_id: delay_id.clone(),
4799 result,
4800 },
4801 },
4802 };
4803 let res = self
4804 .transaction(
4805 {
4806 let execution_id = execution_id.clone();
4807 move |tx| Self::append_response(tx, &execution_id, event.clone())
4808 },
4809 TxType::MultipleWrites,
4810 "append_delay_response",
4811 )
4812 .await;
4813 match res {
4814 Ok(notifier) => {
4815 self.notify_all(vec![notifier], created_at);
4816 Ok(AppendDelayResponseOutcome::Success)
4817 }
4818 Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)) => {
4819 let delay_success = self
4820 .transaction(
4821 move |tx| Self::get_delay_response(tx, &execution_id, &delay_id),
4822 TxType::Other, "get_delay_response",
4824 )
4825 .await?;
4826 match delay_success {
4827 Some(true) => Ok(AppendDelayResponseOutcome::AlreadyFinished),
4828 Some(false) => Ok(AppendDelayResponseOutcome::AlreadyCancelled),
4829 None => Err(DbErrorWrite::Generic(DbErrorGeneric::Uncategorized {
4830 reason: "insert failed yet select did not find the response".into(),
4831 context: SpanTrace::capture(),
4832 source: None,
4833 loc: Location::caller(),
4834 })),
4835 }
4836 }
4837 Err(err) => Err(err),
4838 }
4839 }
4840
4841 #[instrument(level = Level::DEBUG, skip_all)]
4842 async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite> {
4843 trace!("append_backtrace");
4844 self.transaction_fire_forget(
4845 move |tx| Self::append_backtrace(tx, &append),
4846 "append_backtrace",
4847 )
4848 .await;
4849 Ok(())
4850 }
4851
4852 #[instrument(level = Level::DEBUG, skip_all)]
4853 async fn append_backtrace_batch(&self, batch: Vec<BacktraceInfo>) -> Result<(), DbErrorWrite> {
4854 trace!("append_backtrace_batch");
4855 self.transaction_fire_forget(
4856 move |tx| {
4857 for append in &batch {
4858 Self::append_backtrace(tx, append)?;
4859 }
4860 Ok::<_, DbErrorWrite>(())
4861 },
4862 "append_backtrace_batch",
4863 )
4864 .await;
4865 Ok(())
4866 }
4867
4868 #[instrument(level = Level::DEBUG, skip_all)]
4869 async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite> {
4870 trace!("append_log");
4871 self.transaction_fire_forget(move |tx| Self::append_log(tx, &row), "append_log")
4872 .await;
4873 Ok(())
4874 }
4875
4876 #[instrument(level = Level::DEBUG, skip_all)]
4877 async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite> {
4878 trace!("append_log_batch");
4879 let batch = Vec::from(batch);
4880 self.transaction_fire_forget(
4881 move |tx| {
4882 for row in &batch {
4883 Self::append_log(tx, row)?;
4884 }
4885 Ok::<_, DbErrorWrite>(())
4886 },
4887 "append_log_batch",
4888 )
4889 .await;
4890 Ok(())
4891 }
4892
4893 #[instrument(level = Level::TRACE, skip(self))]
4895 async fn get_expired_timers(
4896 &self,
4897 at: DateTime<Utc>,
4898 ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric> {
4899 self.transaction(
4900 move |conn| {
4901 let mut expired_timers = conn.prepare(
4902 "SELECT execution_id, join_set_id, delay_id FROM t_delay WHERE expires_at <= :at",
4903 )?
4904 .query_map(
4905 named_params! {
4906 ":at": at,
4907 },
4908 |row| {
4909 let execution_id = row.get("execution_id")?;
4910 let join_set_id = row.get::<_, JoinSetId>("join_set_id")?;
4911 let delay_id = row.get::<_, DelayId>("delay_id")?;
4912 let delay = ExpiredDelay { execution_id, join_set_id, delay_id };
4913 Ok(ExpiredTimer::Delay(delay))
4914 },
4915 )?
4916 .collect::<Result<Vec<_>, _>>()?;
4917 let expired = conn.prepare(&format!(r#"
4919 SELECT execution_id, last_lock_version, corresponding_version, intermittent_event_count, max_retries, retry_exp_backoff_millis,
4920 executor_id, run_id
4921 FROM t_state
4922 WHERE pending_expires_finished <= :at AND state = "{STATE_LOCKED}"
4923 "#
4924 )
4925 )?
4926 .query_map(
4927 named_params! {
4928 ":at": at,
4929 },
4930 |row| {
4931 let execution_id = row.get("execution_id")?;
4932 let locked_at_version = Version::new(row.get("last_lock_version")?);
4933 let next_version = Version::new(row.get("corresponding_version")?).increment();
4934 let intermittent_event_count = row.get("intermittent_event_count")?;
4935 let max_retries = row.get("max_retries")?;
4936 let retry_exp_backoff_millis = u64::from(row.get::<_, u32>("retry_exp_backoff_millis")?);
4937 let executor_id = row.get("executor_id")?;
4938 let run_id = row.get("run_id")?;
4939 let lock = ExpiredLock {
4940 execution_id,
4941 locked_at_version,
4942 next_version,
4943 intermittent_event_count,
4944 max_retries,
4945 retry_exp_backoff: Duration::from_millis(retry_exp_backoff_millis),
4946 locked_by: LockedBy { executor_id, run_id },
4947 };
4948 Ok(ExpiredTimer::Lock(lock))
4949 }
4950 )?
4951 .collect::<Result<Vec<_>, _>>()?;
4952 expired_timers.extend(expired);
4953 if !expired_timers.is_empty() {
4954 debug!("get_expired_timers found {expired_timers:?}");
4955 }
4956 Ok(expired_timers)
4957 },
4958 TxType::Other, "get_expired_timers"
4960 )
4961 .await
4962 .map_err(to_generic_error)
4963 }
4964
4965 async fn get_execution_event(
4966 &self,
4967 execution_id: &ExecutionId,
4968 version: &Version,
4969 ) -> Result<ExecutionEvent, DbErrorRead> {
4970 let version = version.0;
4971 let execution_id = execution_id.clone();
4972 self.transaction(
4973 move |tx| Self::get_execution_event(tx, &execution_id, version),
4974 TxType::Other, "get_execution_event",
4976 )
4977 .await
4978 }
4979
4980 async fn get_pending_state(
4981 &self,
4982 execution_id: &ExecutionId,
4983 ) -> Result<ExecutionWithState, DbErrorRead> {
4984 let execution_id = execution_id.clone();
4985 Ok(self
4986 .transaction(
4987 move |tx| Self::get_combined_state(tx, &execution_id),
4988 TxType::Other, "get_pending_state",
4990 )
4991 .await?
4992 .execution_with_state)
4993 }
4994}
4995
4996#[cfg(feature = "test")]
4997#[async_trait]
4998impl concepts::storage::DbConnectionTest for SqlitePool {
4999 #[instrument(level = Level::DEBUG, skip(self, response_event), fields(join_set_id = %response_event.join_set_id))]
5000 async fn append_response(
5001 &self,
5002 created_at: DateTime<Utc>,
5003 execution_id: ExecutionId,
5004 response_event: JoinSetResponseEvent,
5005 ) -> Result<(), DbErrorWrite> {
5006 debug!("append_response");
5007 let event = JoinSetResponseEventOuter {
5008 created_at,
5009 event: response_event,
5010 };
5011 let notifier = self
5012 .transaction(
5013 move |tx| Self::append_response(tx, &execution_id, event.clone()),
5014 TxType::Other, "append_response",
5016 )
5017 .await?;
5018 self.notify_all(vec![notifier], created_at);
5019 Ok(())
5020 }
5021}
5022
5023#[cfg(any(test, feature = "tempfile"))]
5024pub mod tempfile {
5025 use super::{SqliteConfig, SqlitePool};
5026 use tempfile::NamedTempFile;
5027
5028 pub async fn sqlite_pool() -> (SqlitePool, Option<NamedTempFile>) {
5029 if let Ok(path) = std::env::var("SQLITE_FILE") {
5030 (
5031 SqlitePool::new(path, SqliteConfig::default())
5032 .await
5033 .unwrap(),
5034 None,
5035 )
5036 } else {
5037 let file = NamedTempFile::new().unwrap();
5038 let path = file.path();
5039 (
5040 SqlitePool::new(path, SqliteConfig::default())
5041 .await
5042 .unwrap(),
5043 Some(file),
5044 )
5045 }
5046 }
5047}
5048
5049#[cfg(test)]
5050mod tests {
5051 use crate::sqlite_dao::{SqlitePool, TxType, tempfile::sqlite_pool};
5052 use assert_matches::assert_matches;
5053 use chrono::DateTime;
5054 use concepts::{
5055 ComponentId, FunctionFqn, Params,
5056 prefixed_ulid::{DEPLOYMENT_ID_DUMMY, EXECUTION_ID_DUMMY},
5057 storage::{CreateRequest, DbErrorWrite, DbErrorWriteNonRetriable, DbPoolCloseable},
5058 };
5059 use rusqlite::named_params;
5060
5061 const SOME_FFQN: FunctionFqn = FunctionFqn::new_static("ns:pkg/ifc", "fn");
5062
5063 #[tokio::test]
5064 async fn failing_ltx_should_be_rolled_back() -> Result<(), DbErrorWrite> {
5065 let created_at = DateTime::from_timestamp_nanos(0);
5066 let (pool, _guard) = sqlite_pool().await;
5067 pool.transaction(
5068 move |tx| {
5069 let req = CreateRequest {
5070 created_at,
5071 execution_id: EXECUTION_ID_DUMMY,
5072 ffqn: SOME_FFQN,
5073 params: Params::empty(),
5074 parent: None,
5075 metadata: concepts::ExecutionMetadata::empty(),
5076 scheduled_at: created_at,
5077 component_id: ComponentId::dummy_activity(),
5078 deployment_id: DEPLOYMENT_ID_DUMMY,
5079 scheduled_by: None,
5080 };
5081 SqlitePool::create_inner(tx, req)?;
5082 SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at)?;
5083 Ok::<_, DbErrorWrite>(())
5084 },
5085 TxType::MultipleWrites,
5086 "create_inner + pause_execution",
5087 )
5088 .await?;
5089
5090 let err = pool
5092 .transaction(
5093 move |tx| SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at),
5094 TxType::MultipleWrites,
5095 "pause_execution",
5096 )
5097 .await
5098 .unwrap_err();
5099 let reason = assert_matches!(err, DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::IllegalState { reason, .. }) => reason);
5100 assert_eq!("cannot pause, execution is already paused", reason.as_ref());
5101
5102 let events = pool.transaction(
5103 move |tx| {
5104 let events =
5105 tx.prepare(
5106 "SELECT created_at, json_value, version FROM t_execution_log WHERE execution_id = :execution_id",
5107 )?
5108 .query_map(
5109 named_params! {
5110 ":execution_id": EXECUTION_ID_DUMMY.to_string(),
5111 },
5112 SqlitePool::map_t_execution_log_row,
5113 )
5114 .map_err(DbErrorWrite::from)?
5115 .collect::<Result<Vec<_>, _>>()?;
5116
5117 Ok::<_, DbErrorWrite>(events)
5118 },
5119 TxType::Other, "get_log",
5121 )
5122 .await?;
5123 assert_eq!(2, events.len());
5124 pool.close().await;
5125 Ok(())
5126 }
5127}