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