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