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