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