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