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