Skip to main content

obeli_sk_db_sqlite/
sqlite_dao.rs

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