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