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, TxType::Other => Ok(()), }
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<usize, 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 OR IGNORE 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 .map_err(DbErrorWrite::from)
2631 }
2632
2633 fn append_log(tx: &Transaction, row: &LogInfoAppendRow) -> Result<(), DbErrorWrite> {
2634 let mut stmt = tx.prepare(
2635 "INSERT INTO t_log (
2636 execution_id,
2637 run_id,
2638 created_at,
2639 level,
2640 message,
2641 stream_type,
2642 payload
2643 ) VALUES (
2644 :execution_id,
2645 :run_id,
2646 :created_at,
2647 :level,
2648 :message,
2649 :stream_type,
2650 :payload
2651 )",
2652 )?;
2653
2654 match &row.log_entry {
2655 LogEntry::Log {
2656 created_at,
2657 level,
2658 message,
2659 } => {
2660 stmt.execute(named_params! {
2661 ":execution_id": row.execution_id,
2662 ":run_id": row.run_id,
2663 ":created_at": created_at,
2664 ":level": *level as u8,
2665 ":message": message,
2666 ":stream_type": Option::<u8>::None,
2667 ":payload": Option::<Vec<u8>>::None,
2668 })?;
2669 }
2670 LogEntry::Stream {
2671 created_at,
2672 payload,
2673 stream_type,
2674 } => {
2675 stmt.execute(named_params! {
2676 ":execution_id": row.execution_id,
2677 ":run_id": row.run_id,
2678 ":created_at": created_at,
2679 ":level": Option::<u8>::None,
2680 ":message": Option::<String>::None,
2681 ":stream_type": *stream_type as u8,
2682 ":payload": payload,
2683 })?;
2684 }
2685 }
2686
2687 Ok(())
2688 }
2689
2690 fn get(
2691 tx: &Transaction,
2692 execution_id: &ExecutionId,
2693 ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
2694 let mut stmt = tx.prepare(
2695 "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2696 execution_id = :execution_id ORDER BY version",
2697 )?;
2698 let events = stmt
2699 .query_map(
2700 named_params! {
2701 ":execution_id": execution_id.to_string(),
2702 },
2703 |row| {
2704 let created_at = row.get("created_at")?;
2705 let event = row
2706 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2707 .map_err(|serde| {
2708 error!("Cannot deserialize {row:?} - {serde:?}");
2709 consistency_rusqlite("cannot deserialize event")
2710 })?
2711 .0;
2712 let version = Version(row.get("version")?);
2713
2714 Ok(ExecutionEvent {
2715 created_at,
2716 event,
2717 backtrace_id: None,
2718 version,
2719 })
2720 },
2721 )?
2722 .collect::<Result<Vec<_>, _>>()?;
2723 if events.is_empty() {
2724 return Err(DbErrorRead::NotFound);
2725 }
2726 let combined_state = Self::get_combined_state(tx, execution_id)?;
2727 let responses = Self::list_responses(tx, execution_id, None)?;
2728 Ok(concepts::storage::ExecutionLog {
2729 execution_id: execution_id.clone(),
2730 events,
2731 responses,
2732 next_version: combined_state.get_next_version_or_finished(), pending_state: combined_state.execution_with_state.pending_state,
2734 component_digest: combined_state.execution_with_state.component_digest,
2735 component_type: combined_state.execution_with_state.component_type,
2736 deployment_id: combined_state.execution_with_state.deployment_id,
2737 })
2738 }
2739
2740 fn get_max_version(
2741 tx: &Transaction,
2742 execution_id: &ExecutionId,
2743 ) -> Result<Version, DbErrorRead> {
2744 tx.prepare("SELECT MAX(version) FROM t_execution_log WHERE execution_id = :execution_id")?
2745 .query_row(
2746 named_params! { ":execution_id": execution_id.to_string() },
2747 |row| row.get::<_, Option<VersionType>>(0),
2748 )
2749 .map(|v| v.map(Version::new).ok_or(DbErrorRead::NotFound))
2750 .map_err(DbErrorRead::from)
2751 .flatten()
2752 }
2753
2754 fn get_max_response_cursor(
2755 tx: &Transaction,
2756 execution_id: &ExecutionId,
2757 ) -> Result<ResponseCursor, DbErrorRead> {
2758 let max_cursor = tx
2759 .prepare("SELECT MAX(seq) FROM t_join_set_response WHERE execution_id = :execution_id")?
2760 .query_row(
2761 named_params! { ":execution_id": execution_id.to_string() },
2762 |row| row.get::<_, Option<u32>>(0),
2763 )?;
2764 let max_cursor = max_cursor.unwrap_or_default();
2766 Ok(ResponseCursor(max_cursor))
2767 }
2768
2769 fn list_execution_events(
2770 tx: &Transaction,
2771 execution_id: &ExecutionId,
2772 pagination: Pagination<VersionType>,
2773 include_backtrace_id: bool,
2774 ) -> Result<Vec<ExecutionEvent>, DbErrorRead> {
2775 let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![];
2776 params.push((":execution_id", Box::new(execution_id.to_string())));
2777
2778 let (cursor, length, rel, is_desc) = match &pagination {
2779 Pagination::NewerThan {
2780 cursor,
2781 length,
2782 including_cursor,
2783 } => (
2784 *cursor,
2785 *length,
2786 if *including_cursor { ">=" } else { ">" },
2787 false,
2788 ),
2789 Pagination::OlderThan {
2790 cursor,
2791 length,
2792 including_cursor,
2793 } => (
2794 *cursor,
2795 *length,
2796 if *including_cursor { "<=" } else { "<" },
2797 true,
2798 ),
2799 };
2800 params.push((":cursor", Box::new(cursor)));
2801
2802 let base_select = if include_backtrace_id {
2803 format!(
2804 "SELECT
2805 log.created_at,
2806 log.json_value,
2807 log.version as version,
2808 bt.version_min_including AS backtrace_id
2809 FROM
2810 t_execution_log AS log
2811 LEFT OUTER JOIN
2812 t_execution_backtrace AS bt ON log.execution_id = bt.execution_id
2813 AND log.version >= bt.version_min_including
2814 AND log.version < bt.version_max_excluding
2815 WHERE
2816 log.execution_id = :execution_id
2817 AND log.version {rel} :cursor"
2818 )
2819 } else {
2820 format!(
2821 "SELECT
2822 created_at, json_value, NULL as backtrace_id, version
2823 FROM t_execution_log WHERE
2824 execution_id = :execution_id AND version {rel} :cursor"
2825 )
2826 };
2827
2828 let order = if is_desc { "DESC" } else { "ASC" };
2829 let mut sql = format!("{base_select} ORDER BY version {order} LIMIT {length}");
2830
2831 if is_desc {
2833 sql = format!("SELECT * FROM ({sql}) ORDER BY version ASC");
2834 }
2835
2836 tx.prepare(&sql)?
2837 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
2838 params
2839 .iter()
2840 .map(|(key, value)| (*key, value.as_ref()))
2841 .collect::<Vec<_>>()
2842 .as_ref(),
2843 |row| {
2844 let created_at = row.get("created_at")?;
2845 let backtrace_id = row
2846 .get::<_, Option<VersionType>>("backtrace_id")?
2847 .map(Version::new);
2848 let version = Version(row.get("version")?);
2849
2850 let event = row
2851 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2852 .map(|event| ExecutionEvent {
2853 created_at,
2854 event: event.0,
2855 backtrace_id,
2856 version,
2857 })
2858 .map_err(|serde| {
2859 error!("Cannot deserialize {row:?} - {serde:?}");
2860 consistency_rusqlite("cannot deserialize")
2861 })?;
2862 Ok(event)
2863 },
2864 )?
2865 .collect::<Result<Vec<_>, _>>()
2866 .map_err(DbErrorRead::from)
2867 }
2868
2869 fn map_t_execution_log_row(row: &Row<'_>) -> Result<ExecutionEvent, rusqlite::Error> {
2870 let created_at = row.get("created_at")?;
2871 let event = row
2872 .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2873 .map_err(|serde| {
2874 error!("Cannot deserialize {row:?} - {serde:?}");
2875 consistency_rusqlite("cannot deserialize event")
2876 })?;
2877 let version = Version(row.get("version")?);
2878
2879 Ok(ExecutionEvent {
2880 created_at,
2881 event: event.0,
2882 backtrace_id: None,
2883 version,
2884 })
2885 }
2886
2887 fn get_execution_event(
2888 tx: &Transaction,
2889 execution_id: &ExecutionId,
2890 version: VersionType,
2891 ) -> Result<ExecutionEvent, DbErrorRead> {
2892 tx.prepare(
2893 "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2894 execution_id = :execution_id AND version = :version",
2895 )?
2896 .query_row(
2897 named_params! {
2898 ":execution_id": execution_id.to_string(),
2899 ":version": version,
2900 },
2901 SqlitePool::map_t_execution_log_row,
2902 )
2903 .map_err(DbErrorRead::from)
2904 }
2905
2906 fn get_last_execution_event(
2907 tx: &Transaction,
2908 execution_id: &ExecutionId,
2909 ) -> Result<ExecutionEvent, DbErrorRead> {
2910 tx.prepare(
2911 "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2912 execution_id = :execution_id ORDER BY version DESC LIMIT 1",
2913 )?
2914 .query_row(
2915 named_params! {
2916 ":execution_id": execution_id.to_string(),
2917 },
2918 SqlitePool::map_t_execution_log_row,
2919 )
2920 .map_err(DbErrorRead::from)
2921 }
2922
2923 fn get_delay_response(
2924 tx: &Transaction,
2925 execution_id: &ExecutionId,
2926 delay_id: &DelayId,
2927 ) -> Result<Option<bool>, DbErrorRead> {
2928 tx.prepare(
2930 "SELECT delay_success \
2931 FROM t_join_set_response \
2932 WHERE \
2933 execution_id = :execution_id AND delay_id = :delay_id
2934 ",
2935 )?
2936 .query_row(
2937 named_params! {
2938 ":execution_id": execution_id.to_string(),
2939 ":delay_id": delay_id.to_string(),
2940 },
2941 |row| {
2942 let delay_success = row.get::<_, bool>("delay_success")?;
2943 Ok(delay_success)
2944 },
2945 )
2946 .optional()
2947 .map_err(DbErrorRead::from)
2948 }
2949
2950 #[instrument(level = Level::TRACE, skip_all)]
2951 fn get_responses_after(
2953 tx: &Transaction,
2954 execution_id: &ExecutionId,
2955 last_response: ResponseCursor,
2956 ) -> Result<Vec<ResponseWithCursor>, DbErrorRead> {
2957 tx.prepare(
2959 "SELECT r.id, r.seq, r.created_at, r.join_set_id, \
2960 r.delay_id, r.delay_success, \
2961 r.child_execution_id, r.finished_version, child.json_value \
2962 FROM t_join_set_response r LEFT OUTER JOIN t_execution_log child ON r.child_execution_id = child.execution_id \
2963 WHERE \
2964 r.seq > :last_response_seq AND \
2965 r.execution_id = :execution_id AND \
2966 ( \
2967 r.finished_version = child.version \
2968 OR r.child_execution_id IS NULL \
2969 ) \
2970 ORDER BY seq",
2971 )
2972 ?
2973 .query_map(
2974 named_params! {
2975 ":last_response_seq": last_response.0,
2976 ":execution_id": execution_id.to_string(),
2977 },
2978 Self::parse_response_with_cursor,
2979 )
2980 ?
2981 .collect::<Result<Vec<_>, _>>()
2982 .map_err(DbErrorRead::from)
2983 }
2984
2985 fn get_pending_of_single_ffqn(
2986 mut stmt: CachedStatement,
2987 batch_size: u32,
2988 pending_at_or_sooner: DateTime<Utc>,
2989 ffqn: &FunctionFqn,
2990 ) -> Result<Vec<(ExecutionId, Version)>, ()> {
2991 stmt.query_map(
2992 named_params! {
2993 ":pending_expires_finished": pending_at_or_sooner,
2994 ":ffqn": ffqn.to_string(),
2995 ":batch_size": batch_size,
2996 },
2997 |row| {
2998 let execution_id = row.get::<_, ExecutionId>("execution_id")?;
2999 let next_version =
3000 Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3001 Ok((execution_id, next_version))
3002 },
3003 )
3004 .map_err(|err| {
3005 warn!("Ignoring consistency error {err:?}");
3006 })?
3007 .collect::<Result<Vec<_>, _>>()
3008 .map_err(|err| {
3009 warn!("Ignoring consistency error {err:?}");
3010 })
3011 }
3012
3013 fn get_pending_by_ffqns(
3015 conn: &Connection,
3016 batch_size: u32,
3017 pending_at_or_sooner: DateTime<Utc>,
3018 ffqns: &[FunctionFqn],
3019 ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3020 let batch_size = usize::try_from(batch_size).expect("16 bit systems are unsupported");
3021 let mut execution_ids_versions = Vec::with_capacity(batch_size);
3022 for ffqn in ffqns {
3023 let needed = batch_size - execution_ids_versions.len();
3024 if needed == 0 {
3025 break;
3026 }
3027 let needed =
3028 u32::try_from(needed).expect("`batch_size`:u32 - usize cannot overflow an u32");
3029 let stmt = conn.prepare_cached(&format!(
3031 r#"
3032 SELECT execution_id, corresponding_version FROM t_state WHERE
3033 state = "{STATE_PENDING_AT}" AND
3034 pending_expires_finished <= :pending_expires_finished AND ffqn = :ffqn
3035 AND lifecycle = 'active'
3036 ORDER BY pending_expires_finished LIMIT :batch_size
3037 "#
3038 ))?;
3039
3040 if let Ok(execs_and_versions) =
3041 Self::get_pending_of_single_ffqn(stmt, needed, pending_at_or_sooner, ffqn)
3042 {
3043 execution_ids_versions.extend(execs_and_versions);
3044 }
3046 }
3047 Ok(execution_ids_versions)
3048 }
3049
3050 fn get_pending_by_ffqns_auto(
3051 conn: &Connection,
3052 batch_size: u32,
3053 pending_at_or_sooner: DateTime<Utc>,
3054 ffqns: &[FunctionFqn],
3055 current_digest: &ComponentDigest,
3056 ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3057 let batch_size = usize::try_from(batch_size).expect("16 bit systems are unsupported");
3058 let mut execution_ids_versions = Vec::with_capacity(batch_size);
3059 for ffqn in ffqns {
3060 let needed = batch_size - execution_ids_versions.len();
3061 if needed == 0 {
3062 break;
3063 }
3064 let mut stmt = conn.prepare_cached(&format!(
3065 r#"
3066 SELECT execution_id, corresponding_version FROM t_state WHERE
3067 state = "{STATE_PENDING_AT}" AND
3068 pending_expires_finished <= :pending_expires_finished AND ffqn = :ffqn
3069 AND lifecycle = 'active'
3070 AND (incompatible_digest IS NULL OR incompatible_digest <> :current_digest)
3071 ORDER BY pending_expires_finished LIMIT :batch_size
3072 "#
3073 ))?;
3074
3075 if let Ok(execs_and_versions) = stmt
3076 .query_map(
3077 named_params! {
3078 ":pending_expires_finished": pending_at_or_sooner,
3079 ":ffqn": ffqn.to_string(),
3080 ":current_digest": current_digest,
3081 ":batch_size": u32::try_from(needed)
3082 .expect("`needed` is <= `batch_size` which is u32"),
3083 },
3084 |row| {
3085 let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3086 let next_version =
3087 Version::new(row.get::<_, VersionType>("corresponding_version")?)
3088 .increment();
3089 Ok((execution_id, next_version))
3090 },
3091 )
3092 .and_then(|rows| rows.collect::<Result<Vec<_>, _>>())
3093 {
3094 execution_ids_versions.extend(execs_and_versions);
3095 if execution_ids_versions.len() == batch_size {
3096 break;
3097 }
3098 }
3099 }
3100 Ok(execution_ids_versions)
3101 }
3102
3103 fn get_pending_by_component_input_digest(
3104 conn: &Connection,
3105 batch_size: u32,
3106 pending_at_or_sooner: DateTime<Utc>,
3107 input_digest: &ComponentDigest,
3108 ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3109 let mut stmt = conn.prepare_cached(&format!(
3110 r#"
3111 SELECT execution_id, corresponding_version FROM t_state WHERE
3112 state = "{STATE_PENDING_AT}" AND
3113 pending_expires_finished <= :pending_expires_finished AND
3114 component_id_input_digest = :component_id_input_digest
3115 AND lifecycle = 'active'
3116 ORDER BY pending_expires_finished LIMIT :batch_size
3117 "#
3118 ))?;
3119
3120 stmt.query_map(
3121 named_params! {
3122 ":pending_expires_finished": pending_at_or_sooner,
3123 ":component_id_input_digest": input_digest,
3124 ":batch_size": batch_size,
3125 },
3126 |row| {
3127 let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3128 let next_version =
3129 Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3130 Ok((execution_id, next_version))
3131 },
3132 )?
3133 .collect::<Result<Vec<_>, _>>()
3134 .map_err(RusqliteError::from)
3135 }
3136
3137 #[instrument(level = Level::TRACE, skip_all)]
3139 fn notify_all(&self, notifiers: Vec<AppendNotifier>, current_time: DateTime<Utc>) {
3140 let (pending_ats, finished_execs, responses) = {
3141 let (mut pending_ats, mut finished_execs, mut responses) =
3142 (Vec::new(), Vec::new(), Vec::new());
3143 for notifier in notifiers {
3144 if let Some(pending_at) = notifier.pending_at {
3145 pending_ats.push(pending_at);
3146 }
3147 if let Some(finished) = notifier.execution_finished {
3148 finished_execs.push(finished);
3149 }
3150 if let Some(response) = notifier.response {
3151 responses.push(response);
3152 }
3153 }
3154 (pending_ats, finished_execs, responses)
3155 };
3156
3157 if !pending_ats.is_empty() {
3159 let guard = self.0.pending_subscribers.lock().unwrap();
3160 for pending_at in pending_ats {
3161 Self::notify_pending_locked(&pending_at, current_time, &guard);
3162 }
3163 }
3164 if !finished_execs.is_empty() {
3167 let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
3168 for finished in finished_execs {
3169 if let Some(listeners_of_exe_id) = guard.remove(&finished.execution_id) {
3170 for (_tag, sender) in listeners_of_exe_id {
3171 let _ = sender.send(finished.retval.clone());
3174 }
3175 }
3176 }
3177 }
3178 if !responses.is_empty() {
3180 let mut guard = self.0.response_subscribers.lock().unwrap();
3181 for (execution_id, _response) in responses {
3182 if let Some((sender, _)) = guard.remove(&execution_id) {
3183 let _ = sender.send(());
3184 }
3185 }
3186 }
3187 }
3188
3189 fn notify_pending_locked(
3190 notifier: &NotifierPendingAt,
3191 current_time: DateTime<Utc>,
3192 ffqn_to_pending_subscription: &std::sync::MutexGuard<PendingFfqnSubscribersHolder>,
3193 ) {
3194 if notifier.scheduled_at <= current_time {
3196 ffqn_to_pending_subscription.notify(notifier);
3197 }
3198 }
3199
3200 fn upgrade_execution_component_single_write(
3201 tx: &Transaction,
3202 execution_id: &ExecutionId,
3203 old: &ComponentDigest,
3204 new: &ComponentDigest,
3205 reason: ComponentUpgradeReason,
3206 ) -> Result<(), DbErrorWrite> {
3207 let combined_state = Self::get_combined_state(tx, execution_id)?;
3208 if combined_state.execution_with_state.component_digest != *old {
3209 return Err(DbErrorWrite::NotFound);
3210 }
3211 let appending_version = combined_state.get_next_version_fail_if_finished()?;
3212 Self::append(
3213 tx,
3214 execution_id,
3215 AppendRequest {
3216 created_at: Utc::now(),
3217 event: ExecutionRequest::ComponentUpgradeFinished {
3218 component_digest: new.clone(),
3219 deployment_id: combined_state.execution_with_state.deployment_id,
3220 outcome: ComponentUpgradeOutcome::Success { reason },
3221 },
3222 },
3223 appending_version,
3224 )?;
3225 Ok(())
3226 }
3227
3228 fn list_logs_tx(
3229 tx: &Transaction,
3230 execution_id: &ExecutionId,
3231 show_derived: bool,
3232 filter: &LogFilter,
3233 pagination: &Pagination<LogCursor>,
3234 ) -> Result<ListLogsResponse, DbErrorRead> {
3235 let length = pagination.length();
3236 let exec_id_str = execution_id.to_string();
3237 let exec_id_filter = if show_derived {
3238 "LIKE :execution_id || '%'"
3239 } else {
3240 "= :execution_id"
3241 };
3242 let mut query = format!(
3243 "SELECT id, run_id, created_at, level, message, stream_type, payload, execution_id
3244 FROM t_log
3245 WHERE execution_id {exec_id_filter}",
3246 );
3247
3248 let cursor = pagination.cursor();
3249 let created_after = filter.created_after();
3250 let created_before = filter.created_before();
3251 let mut params = vec![
3252 (":execution_id", &exec_id_str as &dyn rusqlite::ToSql),
3253 (":cursor", &cursor.0 as &dyn rusqlite::ToSql),
3254 (":length", &length as &dyn rusqlite::ToSql),
3255 ];
3256 if let Some(created_after) = &created_after {
3257 params.push((":created_after", created_after as &dyn rusqlite::ToSql));
3258 }
3259 if let Some(created_before) = &created_before {
3260 params.push((":created_before", created_before as &dyn rusqlite::ToSql));
3261 }
3262
3263 let level_filter = if filter.should_show_logs() {
3265 let levels_str = if !filter.levels().is_empty() {
3266 filter
3267 .levels()
3268 .iter()
3269 .map(|lvl| (*lvl as u8).to_string())
3270 .collect::<Vec<_>>()
3271 .join(",")
3272 } else {
3273 LogLevel::iter()
3274 .map(|lvl| (lvl as u8).to_string())
3275 .collect::<Vec<_>>()
3276 .join(",")
3277 };
3278 Some(format!(" level IN ({levels_str})"))
3279 } else {
3280 None
3281 };
3282 let stream_filter = if filter.should_show_streams() {
3283 let streams_str = if !filter.stream_types().is_empty() {
3284 filter
3285 .stream_types()
3286 .iter()
3287 .map(|st| (*st as u8).to_string())
3288 .collect::<Vec<_>>()
3289 .join(",")
3290 } else {
3291 LogStreamType::iter()
3292 .map(|st| (st as u8).to_string())
3293 .collect::<Vec<_>>()
3294 .join(",")
3295 };
3296 Some(format!(" stream_type IN ({streams_str})"))
3297 } else {
3298 None
3299 };
3300 match (level_filter, stream_filter) {
3301 (Some(level_filter), Some(stream_filter)) => {
3302 write!(&mut query, " AND ({level_filter} OR {stream_filter})")
3303 .expect("writing to string");
3304 }
3305 (Some(level_filter), None) => {
3306 write!(&mut query, " AND {level_filter}").expect("writing to string");
3307 }
3308 (None, Some(stream_filter)) => {
3309 write!(&mut query, " AND {stream_filter}").expect("writing to string");
3310 }
3311 (None, None) => unreachable!("guarded by constructor"),
3312 }
3313
3314 if created_after.is_some() {
3315 query.push_str(" AND created_at > :created_after");
3316 }
3317 if created_before.is_some() {
3318 query.push_str(" AND created_at < :created_before");
3319 }
3320
3321 write!(&mut query, " AND id {} :cursor", pagination.rel()).expect("writing to string");
3323 query.push_str(" ORDER BY id ");
3324 query.push_str(pagination.asc_or_desc());
3325
3326 query.push_str(" LIMIT :length");
3328
3329 let mut stmt = tx.prepare(&query)?;
3330
3331 let items = stmt
3332 .query_map(params.as_slice(), |row| {
3333 let created_at: DateTime<Utc> = row.get("created_at")?;
3334 let run_id = row.get("run_id")?;
3335 let level: Option<u8> = row.get("level")?;
3336 let message: Option<String> = row.get("message")?;
3337 let stream_type: Option<u8> = row.get("stream_type")?;
3338 let payload: Option<Vec<u8>> = row.get("payload")?;
3339 let execution_id_str: String = row.get("execution_id")?;
3340 let execution_id = ExecutionId::from_str(&execution_id_str).map_err(|_| {
3341 consistency_rusqlite(format!("cannot convert ExecutionId {execution_id_str}"))
3342 })?;
3343
3344 let log_entry = match (level, message, stream_type, payload) {
3345 (Some(lvl), Some(msg), None, None) => LogEntry::Log {
3346 created_at,
3347 level: LogLevel::try_from(lvl).map_err(|_| {
3348 consistency_rusqlite(format!("cannot convert {lvl} to LogLevel"))
3349 })?,
3350 message: msg,
3351 },
3352 (None, None, Some(stype), Some(pl)) => LogEntry::Stream {
3353 created_at,
3354 stream_type: LogStreamType::try_from(stype).map_err(|_| {
3355 consistency_rusqlite(format!("cannot convert {stype} to LogStreamType"))
3356 })?,
3357 payload: pl,
3358 },
3359 _ => {
3360 return Err(consistency_rusqlite("invalid t_log row".to_string()));
3361 }
3362 };
3363 Ok(LogEntryRow {
3364 cursor: LogCursor(row.get("id")?),
3365 run_id,
3366 log_entry,
3367 execution_id,
3368 })
3369 })?
3370 .collect::<Result<Vec<_>, _>>()?;
3371
3372 Ok(ListLogsResponse {
3373 next_page: items
3374 .last()
3375 .map(|item| Pagination::NewerThan {
3376 length: pagination.length(),
3377 cursor: item.cursor,
3378 including_cursor: false,
3379 })
3380 .unwrap_or({
3381 if pagination.is_asc() {
3382 *pagination } else {
3384 Pagination::NewerThan {
3386 length: pagination.length(),
3387 cursor: LogCursor(i64::MIN),
3388 including_cursor: false,
3389 }
3390 }
3391 }),
3392 prev_page: match items.first() {
3393 Some(item) => Some(Pagination::OlderThan {
3394 length: pagination.length(),
3395 cursor: item.cursor,
3396 including_cursor: false,
3397 }),
3398 None if pagination.is_asc() && pagination.cursor() != &LogCursor(i64::MIN) => {
3399 Some(pagination.invert())
3401 }
3402 None => None,
3403 },
3404 items,
3405 })
3406 }
3407
3408 fn list_deployment_states(
3409 tx: &Transaction,
3410 current_time: DateTime<Utc>,
3411 pagination: Pagination<Option<DeploymentId>>,
3412 include_deployment_toml: bool,
3413 execution_counts: DeploymentExecutionCounts,
3414 ) -> Result<Vec<DeploymentState>, DbErrorRead> {
3415 let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3416 let deployment_toml_col = if include_deployment_toml {
3417 "d.deployment_toml"
3418 } else {
3419 "NULL AS deployment_toml"
3420 };
3421 let include_execution_counts =
3422 matches!(execution_counts, DeploymentExecutionCounts::Count { .. });
3423 let count_cols = if include_execution_counts {
3425 format!(
3426 r"
3427 COALESCE(SUM(s.state = '{STATE_LOCKED}' AND s.lifecycle = 'active'), 0) AS locked,
3428 COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.lifecycle = 'active' AND s.pending_expires_finished <= :now), 0) AS pending,
3429 COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.lifecycle = 'active' AND s.pending_expires_finished > :now), 0) AS scheduled,
3430 COALESCE(SUM(s.state = '{STATE_BLOCKED_BY_JOIN_SET}' AND s.lifecycle = 'active'), 0) AS blocked,
3431 COALESCE(SUM(s.lifecycle = 'paused'), 0) AS paused,
3432 COALESCE(SUM(s.lifecycle = 'cancelling'), 0) AS cancelling,
3433 COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind = '{RESULT_KIND_JSON_OK}'), 0) AS finished_ok,
3434 COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind = '{RESULT_KIND_JSON_ERROR}'), 0) AS finished_error,
3435 COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind IS NOT NULL
3436 AND s.result_kind NOT IN ('{RESULT_KIND_JSON_OK}', '{RESULT_KIND_JSON_ERROR}')), 0) AS finished_execution_failure,"
3437 )
3438 } else {
3439 "
3440 0 AS locked,
3441 0 AS pending,
3442 0 AS scheduled,
3443 0 AS blocked,
3444 0 AS paused,
3445 0 AS cancelling,
3446 0 AS finished_ok,
3447 0 AS finished_error,
3448 0 AS finished_execution_failure,"
3449 .to_string()
3450 };
3451 let mut sql = format!(
3452 r"
3453 SELECT
3454 d.deployment_id,
3455 d.description,
3456 d.digest,{count_cols}
3457 {deployment_toml_col},
3458 d.created_at,
3459 d.last_active_at,
3460 d.status
3461 FROM t_deployment d{join}",
3462 join = match execution_counts {
3463 DeploymentExecutionCounts::Count { include_derived } => {
3464 let join_top_level = if include_derived {
3465 ""
3466 } else {
3467 " AND s.is_top_level = true"
3468 };
3469 format!(
3470 "\n LEFT JOIN t_state s ON s.deployment_id = d.deployment_id{join_top_level}"
3471 )
3472 }
3473 DeploymentExecutionCounts::Skip => String::new(),
3474 }
3475 );
3476
3477 if include_execution_counts {
3478 params.push((":now", Box::new(current_time)));
3479 }
3480
3481 if let Some(cursor) = pagination.cursor() {
3482 params.push((":cursor", Box::new(*cursor)));
3483 write!(
3484 sql,
3485 " WHERE d.deployment_id {rel} :cursor",
3486 rel = pagination.rel()
3487 )
3488 .expect("writing to string");
3489 }
3490
3491 let (inner_order, outer_order) = if pagination.is_desc() {
3494 ("DESC", "")
3495 } else {
3496 ("ASC", "DESC")
3497 };
3498
3499 if include_execution_counts {
3501 write!(
3502 sql,
3503 " GROUP BY d.deployment_id, d.description, d.digest, d.deployment_toml, d.created_at, d.last_active_at, d.status"
3504 )
3505 .expect("writing to string");
3506 }
3507 write!(
3508 sql,
3509 " ORDER BY d.deployment_id {inner_order} LIMIT {limit}",
3510 limit = pagination.length()
3511 )
3512 .expect("writing to string");
3513
3514 let final_sql = if outer_order.is_empty() {
3515 sql
3516 } else {
3517 format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3518 };
3519
3520 let result: Vec<DeploymentState> = tx
3521 .prepare(&final_sql)?
3522 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3523 params
3524 .iter()
3525 .map(|(k, v)| (*k, v.as_ref()))
3526 .collect::<Vec<_>>()
3527 .as_ref(),
3528 |row| {
3529 let status_str: String = row.get("status")?;
3530 let status = status_str.parse::<DeploymentStatus>().map_err(|_| {
3531 rusqlite::Error::InvalidColumnType(
3532 0,
3533 "status".to_string(),
3534 rusqlite::types::Type::Text,
3535 )
3536 })?;
3537 Ok(DeploymentState {
3538 deployment_id: row.get("deployment_id")?,
3539 description: row.get("description")?,
3540 digest: row.get("digest")?,
3541 locked: row.get("locked")?,
3542 pending: row.get("pending")?,
3543 scheduled: row.get("scheduled")?,
3544 blocked: row.get("blocked")?,
3545 paused: row.get("paused")?,
3546 cancelling: row.get("cancelling")?,
3547 finished_ok: row.get("finished_ok")?,
3548 finished_error: row.get("finished_error")?,
3549 finished_execution_failure: row.get("finished_execution_failure")?,
3550 deployment_toml: row.get("deployment_toml")?,
3551 created_at: row.get("created_at")?,
3552 last_active_at: row.get("last_active_at")?,
3553 status,
3554 })
3555 },
3556 )?
3557 .collect::<Result<Vec<_>, rusqlite::Error>>()
3558 .map_err(DbErrorRead::from)?;
3559
3560 Ok(result)
3561 }
3562
3563 fn insert_deployment_tx(
3564 tx: &Transaction,
3565 record: &DeploymentRecord,
3566 ) -> Result<(), DbErrorWrite> {
3567 assert_eq!(
3568 record.status,
3569 DeploymentStatus::Inactive,
3570 "insert_deployment requires Inactive status"
3571 );
3572 assert!(
3573 record.last_active_at.is_none(),
3574 "insert_deployment requires last_active_at == None"
3575 );
3576 tx.execute(
3577 "INSERT INTO t_deployment \
3578 (deployment_id, description, digest, created_at, status, deployment_toml, obelisk_version, created_by) \
3579 VALUES (:deployment_id, :description, :digest, :created_at, :status, :deployment_toml, :obelisk_version, :created_by)",
3580 rusqlite::named_params! {
3581 ":deployment_id": record.deployment_id.to_string(),
3582 ":description": record.description,
3583 ":digest": record.digest.to_string(),
3584 ":created_at": record.created_at,
3585 ":status": record.status.as_str(),
3586 ":deployment_toml": record.deployment_toml,
3587 ":obelisk_version": record.obelisk_version,
3588 ":created_by": record.created_by,
3589 },
3590 )
3591 .map_err(RusqliteError::from)?;
3592 Self::insert_deployment_files_tx(tx, record.deployment_id, &record.files)?;
3593 Ok(())
3594 }
3595
3596 fn insert_deployment_files_tx(
3597 tx: &Transaction,
3598 deployment_id: DeploymentId,
3599 files: &[DeploymentFileRecord],
3600 ) -> Result<(), DbErrorWrite> {
3601 let mut stmt = tx
3602 .prepare_cached(
3603 "INSERT INTO t_deployment_file (deployment_id, digest, path) \
3604 VALUES (:deployment_id, :digest, :path)",
3605 )
3606 .map_err(RusqliteError::from)?;
3607 for file in files {
3608 stmt.execute(rusqlite::named_params! {
3609 ":deployment_id": deployment_id.to_string(),
3610 ":digest": file.digest.to_string(),
3611 ":path": file.path,
3612 })
3613 .map_err(RusqliteError::from)?;
3614 }
3615 Ok(())
3616 }
3617
3618 fn upsert_component_metadata_tx(
3619 tx: &Transaction,
3620 records: &[ComponentMetadataRecord],
3621 ) -> Result<(), DbErrorWrite> {
3622 let mut stmt = tx
3623 .prepare(
3624 "INSERT OR IGNORE INTO t_component_metadata \
3625 (component_digest, imports_json, exports_json, wit, wit_origin) \
3626 VALUES (:component_digest, :imports_json, :exports_json, :wit, :wit_origin)",
3627 )
3628 .map_err(RusqliteError::from)?;
3629 for record in records {
3630 let imports_json = serde_json::to_string(&record.imports).map_err(|err| {
3631 RusqliteError::from(rusqlite::Error::ToSqlConversionFailure(Box::new(err)))
3632 })?;
3633 let exports_json = serde_json::to_string(&record.exports).map_err(|err| {
3634 RusqliteError::from(rusqlite::Error::ToSqlConversionFailure(Box::new(err)))
3635 })?;
3636 stmt.execute(named_params! {
3637 ":component_digest": record.component_digest.clone(),
3638 ":imports_json": imports_json,
3639 ":exports_json": exports_json,
3640 ":wit": record.wit.clone(),
3641 ":wit_origin": record.wit_origin.clone(),
3642 })
3643 .map_err(RusqliteError::from)?;
3644 }
3645 Ok(())
3646 }
3647
3648 fn insert_deployment_components_tx(
3649 tx: &Transaction,
3650 deployment_id: DeploymentId,
3651 records: &[DeploymentComponentRecord],
3652 ) -> Result<(), DbErrorWrite> {
3653 let mut stmt = tx
3654 .prepare(
3655 "INSERT OR IGNORE INTO t_deployment_component \
3656 (deployment_id, component_name, component_type, component_digest) \
3657 VALUES (:deployment_id, :component_name, :component_type, :component_digest)",
3658 )
3659 .map_err(RusqliteError::from)?;
3660 for record in records {
3661 debug_assert_eq!(record.deployment_id, deployment_id);
3662 stmt.execute(named_params! {
3663 ":deployment_id": deployment_id.to_string(),
3664 ":component_name": record.component_name.to_string(),
3665 ":component_type": record.component_type.to_string(),
3666 ":component_digest": record.component_digest.clone(),
3667 })
3668 .map_err(RusqliteError::from)?;
3669 }
3670 Ok(())
3671 }
3672
3673 fn compute_file_digest(content: &[u8]) -> ContentDigest {
3674 let hash: [u8; 32] = Sha256::digest(content).into();
3675 ContentDigest(Digest(hash))
3676 }
3677
3678 fn upload_file_tx(
3679 tx: &Transaction,
3680 digest: &ContentDigest,
3681 content: &[u8],
3682 ) -> Result<(), DbErrorWrite> {
3683 let actual = Self::compute_file_digest(content);
3684 if &actual != digest {
3685 return Err(DbErrorWriteNonRetriable::ValidationFailed(
3686 format!("uploaded file digest mismatch: expected {digest}, got {actual}").into(),
3687 )
3688 .into());
3689 }
3690 let size = i64::try_from(content.len()).map_err(|err| DbErrorGeneric::Uncategorized {
3691 reason: format!("deployment file too large: {err}").into(),
3692 context: SpanTrace::capture(),
3693 source: Some(Arc::new(err)),
3694 loc: Location::caller(),
3695 })?;
3696 tx.execute(
3697 "INSERT INTO t_file (digest, content, size) VALUES (:digest, :content, :size) \
3698 ON CONFLICT (digest) DO NOTHING",
3699 rusqlite::named_params! {
3700 ":digest": digest.to_string(),
3701 ":content": content,
3702 ":size": size,
3703 },
3704 )
3705 .map_err(RusqliteError::from)?;
3706 Ok(())
3707 }
3708
3709 fn get_file_tx(
3710 tx: &Transaction,
3711 digest: &ContentDigest,
3712 ) -> Result<Option<Vec<u8>>, DbErrorRead> {
3713 tx.query_row(
3714 "SELECT content FROM t_file WHERE digest = :digest",
3715 rusqlite::named_params! { ":digest": digest.to_string() },
3716 |row| row.get("content"),
3717 )
3718 .optional()
3719 .map_err(|err| DbErrorRead::from(RusqliteError::from(err)))
3720 }
3721
3722 fn missing_digests_tx(
3723 tx: &Transaction,
3724 deployment_id: DeploymentId,
3725 ) -> Result<Vec<ContentDigest>, DbErrorRead> {
3726 tx.prepare(
3727 "SELECT df.digest \
3728 FROM t_deployment_file df \
3729 LEFT JOIN t_file f ON f.digest = df.digest \
3730 WHERE df.deployment_id = :deployment_id AND f.digest IS NULL \
3731 ORDER BY df.digest",
3732 )?
3733 .query_map(
3734 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3735 |row| row.get("digest"),
3736 )?
3737 .collect::<Result<Vec<_>, rusqlite::Error>>()
3738 .map_err(DbErrorRead::from)
3739 }
3740
3741 fn list_deployment_files_tx(
3742 tx: &Transaction,
3743 deployment_id: DeploymentId,
3744 ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead> {
3745 tx.prepare(
3746 "SELECT df.path, df.digest, f.size FROM t_deployment_file df \
3749 LEFT JOIN t_file f ON f.digest = df.digest \
3750 WHERE df.deployment_id = :deployment_id \
3751 ORDER BY df.path, df.digest",
3752 )?
3753 .query_map(
3754 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3755 |row| {
3756 let path: String = row.get("path")?;
3757 let digest: ContentDigest = row.get("digest")?;
3758 let size = if let Some(size) = row.get::<_, Option<i64>>("size")? {
3759 u64::try_from(size)
3761 .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(2, size))?
3762 } else {
3763 warn!(%path, %digest, "deployment file metadata missing from t_file, reporting size 0");
3764 0
3765 };
3766 Ok(DeploymentFileRecord { path, digest, size })
3767 },
3768 )?
3769 .collect::<Result<Vec<_>, rusqlite::Error>>()
3770 .map_err(DbErrorRead::from)
3771 }
3772
3773 fn activate_deployment_tx(
3774 tx: &Transaction,
3775 deployment_id: DeploymentId,
3776 now: DateTime<Utc>,
3777 ) -> Result<(), DbErrorWrite> {
3778 tx.execute(
3780 "UPDATE t_deployment SET status = 'inactive' WHERE status IN ('active', 'enqueued')",
3781 [],
3782 )
3783 .map_err(RusqliteError::from)?;
3784 let rows = tx
3786 .execute(
3787 "UPDATE t_deployment SET status = 'active', last_active_at = :now WHERE deployment_id = :deployment_id",
3788 rusqlite::named_params! {
3789 ":now": now,
3790 ":deployment_id": deployment_id.to_string(),
3791 },
3792 )
3793 .map_err(RusqliteError::from)?;
3794 if rows == 0 {
3795 return Err(DbErrorWrite::NotFound);
3796 }
3797 Ok(())
3798 }
3799
3800 fn enqueue_deployment_tx(
3801 tx: &Transaction,
3802 deployment_id: DeploymentId,
3803 ) -> Result<EnqueueOutcome, DbErrorWrite> {
3804 let status_opt: Option<String> = tx
3805 .query_row(
3806 "SELECT status FROM t_deployment WHERE deployment_id = :deployment_id",
3807 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3808 |row| row.get(0),
3809 )
3810 .optional()
3811 .map_err(RusqliteError::from)?;
3812 let status = status_opt.as_deref();
3813 if status.is_none() {
3814 return Err(DbErrorWrite::NotFound);
3815 }
3816 tx.execute(
3818 "UPDATE t_deployment SET status = 'inactive' WHERE status = 'enqueued'",
3819 [],
3820 )
3821 .map_err(RusqliteError::from)?;
3822 if status == Some("active") {
3824 return Ok(EnqueueOutcome::AlreadyActive);
3825 }
3826 let rows = tx
3828 .execute(
3829 "UPDATE t_deployment SET status = 'enqueued' WHERE deployment_id = :deployment_id",
3830 rusqlite::named_params! {
3831 ":deployment_id": deployment_id.to_string(),
3832 },
3833 )
3834 .map_err(RusqliteError::from)?;
3835 if rows == 0 {
3836 return Err(DbErrorWrite::NotFound);
3837 }
3838 Ok(EnqueueOutcome::Enqueued)
3839 }
3840
3841 fn get_deployment_tx(
3842 tx: &Transaction,
3843 deployment_id: DeploymentId,
3844 ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3845 let Some(record) = tx
3846 .query_row(
3847 "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3848 FROM t_deployment WHERE deployment_id = :deployment_id",
3849 rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3850 deployment_record_from_row,
3851 )
3852 .optional()
3853 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
3854 else {
3855 return Ok(None);
3856 };
3857 Self::with_deployment_files_tx(tx, record).map(Some)
3858 }
3859
3860 #[cfg(feature = "test")]
3861 fn get_active_deployment_tx(tx: &Transaction) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3862 let Some(record) = tx
3863 .query_row(
3864 "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3865 FROM t_deployment WHERE status = 'active' LIMIT 1",
3866 [],
3867 deployment_record_from_row,
3868 )
3869 .optional()
3870 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
3871 else {
3872 return Ok(None);
3873 };
3874 Self::with_deployment_files_tx(tx, record).map(Some)
3875 }
3876
3877 fn list_deployments_tx(
3878 tx: &Transaction,
3879 pagination: Pagination<Option<DeploymentId>>,
3880 ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
3881 let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3882 let mut sql = String::from(
3883 "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3884 FROM t_deployment",
3885 );
3886
3887 if let Some(cursor) = pagination.cursor() {
3888 params.push((":cursor", Box::new(*cursor)));
3889 write!(
3890 sql,
3891 " WHERE deployment_id {rel} :cursor",
3892 rel = pagination.rel()
3893 )
3894 .expect("writing to string");
3895 }
3896
3897 let (inner_order, outer_order) = if pagination.is_desc() {
3898 ("DESC", "")
3899 } else {
3900 ("ASC", "DESC")
3901 };
3902
3903 write!(
3904 sql,
3905 " ORDER BY deployment_id {inner_order} LIMIT {limit}",
3906 limit = pagination.length()
3907 )
3908 .expect("writing to string");
3909
3910 let final_sql = if outer_order.is_empty() {
3911 sql
3912 } else {
3913 format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3914 };
3915
3916 let mut result: Vec<DeploymentRecord> = tx
3917 .prepare(&final_sql)?
3918 .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3919 params
3920 .iter()
3921 .map(|(k, v)| (*k, v.as_ref()))
3922 .collect::<Vec<_>>()
3923 .as_ref(),
3924 deployment_record_from_row,
3925 )?
3926 .collect::<Result<Vec<_>, rusqlite::Error>>()
3927 .map_err(DbErrorRead::from)?;
3928 for record in &mut result {
3929 record.files = Self::list_deployment_files_tx(tx, record.deployment_id)?;
3930 }
3931
3932 Ok(result)
3933 }
3934
3935 fn with_deployment_files_tx(
3936 tx: &Transaction,
3937 mut record: DeploymentRecord,
3938 ) -> Result<DeploymentRecord, DbErrorRead> {
3939 record.files = Self::list_deployment_files_tx(tx, record.deployment_id)?;
3940 Ok(record)
3941 }
3942
3943 fn pause_execution(
3944 tx: &Transaction,
3945 execution_id: &ExecutionId,
3946 paused_at: DateTime<Utc>,
3947 ) -> Result<Version, DbErrorWrite> {
3948 let combined_state = Self::get_combined_state(tx, execution_id)?;
3949 let mut appending_version = combined_state.get_next_version_fail_if_finished()?;
3950 debug!("Pausing with {appending_version}");
3951 if combined_state.reject_locked_activities()? {
3952 (appending_version, _) = Self::append(
3953 tx,
3954 execution_id,
3955 AppendRequest {
3956 created_at: paused_at,
3957 event: ExecutionRequest::Unlocked(Unlocked {
3958 unlocked_at: paused_at, reason: "paused".into(),
3960 }),
3961 },
3962 appending_version,
3963 )?;
3964 }
3965 let (next_version, _notifier) = Self::append(
3966 tx,
3967 execution_id,
3968 AppendRequest {
3969 created_at: paused_at,
3970 event: ExecutionRequest::Paused,
3971 },
3972 appending_version,
3973 )?;
3974 Ok(next_version)
3975 }
3976
3977 fn unpause_execution(
3978 tx: &Transaction,
3979 execution_id: &ExecutionId,
3980 paused_at: DateTime<Utc>,
3981 ) -> Result<Version, DbErrorWrite> {
3982 let combined_state = Self::get_combined_state(tx, execution_id)?;
3983 let appending_version = combined_state.get_next_version_fail_if_finished()?;
3984 debug!("Unpausing with {appending_version}");
3985 let (next_version, _) = Self::append(
3986 tx,
3987 execution_id,
3988 AppendRequest {
3989 created_at: paused_at,
3990 event: ExecutionRequest::Unpaused,
3991 },
3992 appending_version,
3993 )?;
3994 Ok(next_version)
3995 }
3996
3997 fn cancel_workflow(
3998 tx: &Transaction,
3999 execution_id: &ExecutionId,
4000 cancelled_at: DateTime<Utc>,
4001 ) -> Result<CancelOutcome, DbErrorWrite> {
4002 let combined_state = Self::get_combined_state(tx, execution_id)?;
4003 if let Some(outcome) = combined_state.cancel_short_circuit() {
4004 return Ok(outcome);
4005 }
4006 Self::append_cancellation_requested(
4007 tx,
4008 execution_id,
4009 cancelled_at,
4010 &combined_state,
4011 CancellationFfqnCheck::Required,
4012 )
4013 }
4014
4015 fn append_cancellation_requested(
4016 tx: &Transaction,
4017 execution_id: &ExecutionId,
4018 cancelled_at: DateTime<Utc>,
4019 combined_state: &CombinedState,
4020 ffqn_check: CancellationFfqnCheck,
4021 ) -> Result<CancelOutcome, DbErrorWrite> {
4022 if matches!(ffqn_check, CancellationFfqnCheck::Required) {
4023 combined_state.assert_cancellable_workflow_ffqn()?;
4024 }
4025 Self::append(
4026 tx,
4027 execution_id,
4028 AppendRequest {
4029 created_at: cancelled_at,
4030 event: ExecutionRequest::CancellationRequested,
4031 },
4032 combined_state.get_next_version_assert_not_finished(),
4033 )?;
4034 Ok(CancelOutcome::Cancelled)
4035 }
4036
4037 fn append_activity_cancellation_requested_tx(
4038 tx: &Transaction,
4039 execution_id: &ExecutionId,
4040 cancelled_at: DateTime<Utc>,
4041 combined_state: &CombinedState,
4042 ) -> Result<CancelOutcome, DbErrorWrite> {
4043 match &combined_state.execution_with_state.pending_state {
4044 PendingState::Finished(finished) => {
4045 if finished.result_kind
4046 == PendingStateFinishedResultKind::Err(
4047 PendingStateFinishedError::ExecutionFailure(
4048 ExecutionFailureKind::Cancelled,
4049 ),
4050 )
4051 {
4052 return Ok(CancelOutcome::Cancelled);
4053 }
4054 return Ok(CancelOutcome::AlreadyFinished);
4055 }
4056 PendingState::Cancelling(_) => return Ok(CancelOutcome::Cancelled),
4057 _ => {}
4058 }
4059 Self::append_cancellation_requested(
4060 tx,
4061 execution_id,
4062 cancelled_at,
4063 combined_state,
4064 CancellationFfqnCheck::Skipped,
4065 )
4066 }
4067}
4068
4069#[async_trait]
4070impl DbExecutor for SqlitePool {
4071 #[instrument(level = Level::TRACE, skip(self))]
4072 async fn lock_pending_by_ffqns(
4073 &self,
4074 batch_size: u32,
4075 pending_at_or_sooner: DateTime<Utc>,
4076 ffqns: Arc<[FunctionFqn]>,
4077 created_at: DateTime<Utc>,
4078 component_id: ComponentId,
4079 deployment_id: DeploymentId,
4080 executor_id: ExecutorId,
4081 lock_expires_at: DateTime<Utc>,
4082 run_id: RunId,
4083 retry_config: ComponentRetryConfig,
4084 ) -> Result<LockPendingResponse, DbErrorWrite> {
4085 let execution_ids_versions = self
4086 .transaction(
4087 move |conn| {
4088 Self::get_pending_by_ffqns(conn, batch_size, pending_at_or_sooner, &ffqns)
4089 },
4090 TxType::Other, "lock_pending_by_ffqns_get",
4092 )
4093 .await
4094 .map_err(to_generic_error)?;
4095 if execution_ids_versions.is_empty() {
4096 Ok(vec![])
4097 } else {
4098 debug!("Locking {execution_ids_versions:?}");
4099 self.transaction(
4100 move |tx| {
4101 let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4102 for (execution_id, version) in &execution_ids_versions {
4104 locked_execs.push(Self::lock_single_execution(
4105 tx,
4106 created_at,
4107 &component_id,
4108 true,
4109 deployment_id,
4110 execution_id,
4111 run_id,
4112 version,
4113 executor_id,
4114 lock_expires_at,
4115 retry_config,
4116 )?);
4117 }
4118 Ok::<_, DbErrorWrite>(locked_execs)
4119 },
4120 TxType::MultipleWrites,
4121 "lock_pending_by_ffqns_one",
4122 )
4123 .await
4124 }
4125 }
4126
4127 #[instrument(level = Level::TRACE, skip(self))]
4128 async fn lock_pending_by_ffqns_auto(
4129 &self,
4130 batch_size: u32,
4131 pending_at_or_sooner: DateTime<Utc>,
4132 ffqns: Arc<[FunctionFqn]>,
4133 created_at: DateTime<Utc>,
4134 component_id: ComponentId,
4135 deployment_id: DeploymentId,
4136 executor_id: ExecutorId,
4137 lock_expires_at: DateTime<Utc>,
4138 run_id: RunId,
4139 retry_config: ComponentRetryConfig,
4140 ) -> Result<LockPendingResponse, DbErrorWrite> {
4141 let current_digest = component_id.component_digest.clone();
4142 let execution_ids_versions = self
4143 .transaction(
4144 move |conn| {
4145 Self::get_pending_by_ffqns_auto(
4146 conn,
4147 batch_size,
4148 pending_at_or_sooner,
4149 &ffqns,
4150 ¤t_digest,
4151 )
4152 },
4153 TxType::Other,
4154 "lock_pending_by_ffqns_auto_get",
4155 )
4156 .await
4157 .map_err(to_generic_error)?;
4158 if execution_ids_versions.is_empty() {
4159 Ok(vec![])
4160 } else {
4161 debug!("Auto-locking {execution_ids_versions:?}");
4162 self.transaction(
4163 move |tx| {
4164 let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4165 for (execution_id, version) in &execution_ids_versions {
4166 locked_execs.push(Self::lock_single_execution(
4167 tx,
4168 created_at,
4169 &component_id,
4170 false,
4171 deployment_id,
4172 execution_id,
4173 run_id,
4174 version,
4175 executor_id,
4176 lock_expires_at,
4177 retry_config,
4178 )?);
4179 }
4180 Ok::<_, DbErrorWrite>(locked_execs)
4181 },
4182 TxType::MultipleWrites,
4183 "lock_pending_by_ffqns_auto_one",
4184 )
4185 .await
4186 }
4187 }
4188
4189 #[instrument(level = Level::TRACE, skip(self))]
4190 async fn lock_pending_by_component_digest(
4191 &self,
4192 batch_size: u32,
4193 pending_at_or_sooner: DateTime<Utc>,
4194 component_id: &ComponentId,
4195 deployment_id: DeploymentId,
4196 created_at: DateTime<Utc>,
4197 executor_id: ExecutorId,
4198 lock_expires_at: DateTime<Utc>,
4199 run_id: RunId,
4200 retry_config: ComponentRetryConfig,
4201 ) -> Result<LockPendingResponse, DbErrorWrite> {
4202 let component_id = component_id.clone();
4203 let execution_ids_versions = self
4204 .transaction(
4205 {
4206 let component_id = component_id.clone();
4207 move |conn| {
4208 Self::get_pending_by_component_input_digest(
4209 conn,
4210 batch_size,
4211 pending_at_or_sooner,
4212 &component_id.component_digest,
4213 )
4214 }
4215 },
4216 TxType::Other, "lock_pending_by_component_id_get",
4218 )
4219 .await
4220 .map_err(to_generic_error)?;
4221 if execution_ids_versions.is_empty() {
4222 Ok(vec![])
4223 } else {
4224 debug!("Locking {execution_ids_versions:?}");
4225 self.transaction(
4226 move |tx| {
4227 let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4228 for (execution_id, version) in &execution_ids_versions {
4230 locked_execs.push(Self::lock_single_execution(
4231 tx,
4232 created_at,
4233 &component_id,
4234 true,
4235 deployment_id,
4236 execution_id,
4237 run_id,
4238 version,
4239 executor_id,
4240 lock_expires_at,
4241 retry_config,
4242 )?);
4243 }
4244 Ok::<_, DbErrorWrite>(locked_execs)
4245 },
4246 TxType::MultipleWrites,
4247 "lock_pending_by_component_id_one",
4248 )
4249 .await
4250 }
4251 }
4252
4253 #[cfg(feature = "test")]
4254 #[instrument(level = Level::DEBUG, skip(self))]
4255 async fn lock_one(
4256 &self,
4257 created_at: DateTime<Utc>,
4258 component_id: ComponentId,
4259 deployment_id: DeploymentId,
4260 execution_id: &ExecutionId,
4261 run_id: RunId,
4262 version: Version,
4263 executor_id: ExecutorId,
4264 lock_expires_at: DateTime<Utc>,
4265 retry_config: ComponentRetryConfig,
4266 ) -> Result<LockedExecution, DbErrorWrite> {
4267 debug!(%execution_id, "lock_one");
4268 let execution_id = execution_id.clone();
4269 self.transaction(
4270 move |tx| {
4271 Self::lock_single_execution(
4272 tx,
4273 created_at,
4274 &component_id,
4275 true,
4276 deployment_id,
4277 &execution_id,
4278 run_id,
4279 &version,
4280 executor_id,
4281 lock_expires_at,
4282 retry_config,
4283 )
4284 },
4285 TxType::MultipleWrites, "lock_inner",
4287 )
4288 .await
4289 }
4290
4291 #[instrument(level = Level::DEBUG, skip(self, req))]
4292 async fn append(
4293 &self,
4294 execution_id: ExecutionId,
4295 version: Version,
4296 req: AppendRequest,
4297 ) -> Result<AppendResponse, DbErrorWrite> {
4298 debug!(%req, "append");
4299 trace!(?req, "append");
4300 let created_at = req.created_at;
4301 let (version, notifier) = self
4302 .transaction(
4303 move |tx| Self::append(tx, &execution_id, req.clone(), version.clone()),
4304 TxType::MultipleWrites, "append",
4306 )
4307 .await?;
4308 self.notify_all(vec![notifier], created_at);
4309 Ok(version)
4310 }
4311
4312 #[instrument(level = Level::DEBUG, skip_all)]
4313 async fn append_batch_respond_to_parent(
4314 &self,
4315 events: AppendEventsToExecution,
4316 response: AppendResponseToExecution,
4317 current_time: DateTime<Utc>,
4318 ) -> Result<AppendBatchResponse, DbErrorWrite> {
4319 debug!("append_batch_respond_to_parent");
4320 if events.execution_id == response.parent_execution_id {
4321 return Err(DbErrorWrite::NonRetriable(
4324 DbErrorWriteNonRetriable::ValidationFailed(
4325 "Parameters `execution_id` and `parent_execution_id` cannot be the same".into(),
4326 ),
4327 ));
4328 }
4329 if events.batch.is_empty() {
4330 error!("Batch cannot be empty");
4331 return Err(DbErrorWrite::NonRetriable(
4332 DbErrorWriteNonRetriable::ValidationFailed("batch cannot be empty".into()),
4333 ));
4334 }
4335 let (version, notifiers) = {
4336 self.transaction(
4337 move |tx| {
4338 let mut version = events.version.clone();
4339 let mut notifier_of_child = None;
4340 for append_request in &events.batch {
4341 let (v, n) = Self::append(
4342 tx,
4343 &events.execution_id,
4344 append_request.clone(),
4345 version,
4346 )?;
4347 version = v;
4348 notifier_of_child = Some(n);
4349 }
4350
4351 let pending_at_parent = Self::append_response(
4352 tx,
4353 &response.parent_execution_id,
4354 JoinSetResponseEventOuter {
4355 created_at: response.created_at,
4356 event: JoinSetResponseEvent {
4357 join_set_id: response.join_set_id.clone(),
4358 event: JoinSetResponse::ChildExecutionFinished {
4359 child_execution_id: response.child_execution_id.clone(),
4360 finished_version: response.finished_version.clone(),
4361 result: response.result.clone(),
4362 },
4363 },
4364 },
4365 )?;
4366 Ok::<_, DbErrorWrite>((
4367 version,
4368 vec![
4369 notifier_of_child.expect("checked that the batch is not empty"),
4370 pending_at_parent,
4371 ],
4372 ))
4373 },
4374 TxType::MultipleWrites,
4375 "append_batch_respond_to_parent",
4376 )
4377 .await?
4378 };
4379 self.notify_all(notifiers, current_time);
4380 Ok(version)
4381 }
4382
4383 #[instrument(level = Level::TRACE, skip(self, timeout_fut))]
4386 async fn wait_for_pending_by_ffqn(
4387 &self,
4388 pending_at_or_sooner: DateTime<Utc>,
4389 ffqns: Arc<[FunctionFqn]>,
4390 current_digest: Option<ComponentDigest>,
4391 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4392 ) {
4393 let unique_tag: u64 = rand::random();
4394 let (sender, mut receiver) = mpsc::channel(1); {
4396 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4397 for ffqn in ffqns.as_ref() {
4398 pending_subscribers.insert_ffqn(ffqn.clone(), (sender.clone(), unique_tag));
4399 }
4400 }
4401 async {
4402 let Ok(execution_ids_versions) = self
4403 .transaction(
4404 {
4405 let ffqns = ffqns.clone();
4406 move |conn| {
4407 if let Some(current_digest) = ¤t_digest {
4408 Self::get_pending_by_ffqns_auto(
4409 conn,
4410 1,
4411 pending_at_or_sooner,
4412 ffqns.as_ref(),
4413 current_digest,
4414 )
4415 } else {
4416 Self::get_pending_by_ffqns(
4417 conn,
4418 1,
4419 pending_at_or_sooner,
4420 ffqns.as_ref(),
4421 )
4422 }
4423 }
4424 },
4425 TxType::Other, "get_pending_by_ffqns",
4427 )
4428 .await
4429 else {
4430 trace!(
4431 "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4432 );
4433 timeout_fut.await;
4434 return;
4435 };
4436 if !execution_ids_versions.is_empty() {
4437 trace!("Not waiting, database already contains new pending executions");
4438 return;
4439 }
4440 tokio::select! { _ = receiver.recv() => {
4442 trace!("Received a notification");
4443 }
4444 () = timeout_fut => {
4445 }
4446 }
4447 }.await;
4448 {
4450 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4451 for ffqn in ffqns.as_ref() {
4452 match pending_subscribers.remove_ffqn(ffqn) {
4453 Some((_, tag)) if tag == unique_tag => {
4454 }
4456 Some(other) => {
4457 pending_subscribers.insert_ffqn(ffqn.clone(), other);
4459 }
4460 None => {
4461 }
4463 }
4464 }
4465 }
4466 }
4467
4468 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4471 async fn wait_for_pending_by_component_digest(
4472 &self,
4473 pending_at_or_sooner: DateTime<Utc>,
4474 component_digest: &ComponentDigest,
4475 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4476 ) {
4477 let unique_tag: u64 = rand::random();
4478 let (sender, mut receiver) = mpsc::channel(1); {
4480 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4481 pending_subscribers
4482 .insert_by_component(component_digest.clone(), (sender.clone(), unique_tag));
4483 }
4484 async {
4485 let Ok(execution_ids_versions) = self
4486 .transaction(
4487 {
4488 let input_digest = component_digest.clone();
4489 move |conn| Self::get_pending_by_component_input_digest(conn, 1, pending_at_or_sooner, &input_digest)
4490 },
4491 TxType::Other, "get_pending_by_component_input_digest",
4493 )
4494 .await
4495 else {
4496 trace!(
4497 "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4498 );
4499 timeout_fut.await;
4500 return;
4501 };
4502 if !execution_ids_versions.is_empty() {
4503 trace!("Not waiting, database already contains new pending executions");
4504 return;
4505 }
4506 tokio::select! { _ = receiver.recv() => {
4508 trace!("Received a notification");
4509 }
4510 () = timeout_fut => {
4511 }
4512 }
4513 }.await;
4514 {
4516 let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4517
4518 match pending_subscribers.remove_by_component(component_digest) {
4519 Some((_, tag)) if tag == unique_tag => {
4520 }
4522 Some(other) => {
4523 pending_subscribers.insert_by_component(component_digest.clone(), other);
4525 }
4526 None => {
4527 }
4529 }
4530 }
4531 }
4532
4533 async fn get_last_execution_event(
4534 &self,
4535 execution_id: &ExecutionId,
4536 ) -> Result<ExecutionEvent, DbErrorRead> {
4537 let execution_id = execution_id.clone();
4538 self.transaction(
4539 move |tx| Self::get_last_execution_event(tx, &execution_id),
4540 TxType::Other, "get_last_execution_event",
4542 )
4543 .await
4544 }
4545
4546 #[instrument(skip(self))]
4547 async fn append_activity_cancellation_requested(
4548 &self,
4549 execution_id: &ExecutionId,
4550 cancelled_at: DateTime<Utc>,
4551 ) -> Result<CancelOutcome, DbErrorWrite> {
4552 let execution_id = execution_id.clone();
4553 self.transaction(
4554 move |tx| {
4555 let combined_state = Self::get_combined_state(tx, &execution_id)?;
4556 SqlitePool::append_activity_cancellation_requested_tx(
4557 tx,
4558 &execution_id,
4559 cancelled_at,
4560 &combined_state,
4561 )
4562 },
4563 TxType::MultipleWrites,
4564 "append_activity_cancellation_requested",
4565 )
4566 .await
4567 }
4568
4569 #[instrument(skip(self))]
4570 async fn cancel_workflow(
4571 &self,
4572 execution_id: &ExecutionId,
4573 cancelled_at: DateTime<Utc>,
4574 ) -> Result<CancelOutcome, DbErrorWrite> {
4575 let execution_id = execution_id.clone();
4576 self.transaction(
4577 move |tx| SqlitePool::cancel_workflow(tx, &execution_id, cancelled_at),
4578 TxType::MultipleWrites,
4579 "cancel_workflow",
4580 )
4581 .await
4582 }
4583}
4584
4585#[async_trait]
4586impl DbExternalApi for SqlitePool {
4587 #[instrument(skip(self))]
4588 async fn get_backtrace(
4589 &self,
4590 execution_id: &ExecutionId,
4591 filter: BacktraceFilter,
4592 ) -> Result<BacktraceInfo, DbErrorRead> {
4593 debug!("get_backtrace");
4594 let execution_id = execution_id.clone();
4595
4596 self.transaction(
4597 move |tx| {
4598 let select = "SELECT component_id, version_min_including, version_max_excluding, wasm_backtrace FROM t_execution_backtrace e \
4599 INNER JOIN t_wasm_backtrace w ON e.backtrace_hash = w.backtrace_hash \
4600 WHERE execution_id = :execution_id";
4601 let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![(":execution_id", Box::new(execution_id.to_string()))];
4602 let select = match &filter {
4603 BacktraceFilter::Specific(version) =>{
4604 params.push((":version", Box::new(version.0)));
4605 format!("{select} AND version_min_including <= :version AND version_max_excluding > :version")
4606 },
4607 BacktraceFilter::First => format!("{select} ORDER BY version_min_including LIMIT 1"),
4608 BacktraceFilter::Last => format!("{select} ORDER BY version_min_including DESC LIMIT 1")
4609 };
4610 tx
4611 .prepare(&select)
4612 ?
4613 .query_row::<_, &[(&'static str, &dyn ToSql)], _>(
4614 params
4615 .iter()
4616 .map(|(key, value)| (*key, value.as_ref()))
4617 .collect::<Vec<_>>()
4618 .as_ref(),
4619 |row| {
4620 Ok(BacktraceInfo {
4621 execution_id: execution_id.clone(),
4622 component_id: row.get::<_, JsonWrapper<_> >("component_id")?.0,
4623 version_min_including: Version::new(row.get::<_, VersionType>("version_min_including")?),
4624 version_max_excluding: Version::new(row.get::<_, VersionType>("version_max_excluding")?),
4625 wasm_backtrace: row.get::<_, JsonWrapper<_>>("wasm_backtrace")?.0,
4626 })
4627 },
4628 ).map_err(DbErrorRead::from)
4629 },
4630 TxType::Other, "get_last_backtrace",
4632 ).await
4633 }
4634
4635 #[instrument(skip_all)]
4636 async fn upsert_source_mapping(
4637 &self,
4638 component_digest: &ComponentDigest,
4639 frame_key: &str,
4640 is_suffix: bool,
4641 digest: &ContentDigest,
4642 ) -> Result<(), DbErrorWrite> {
4643 let component_digest = component_digest.clone();
4644 let frame_key = frame_key.to_owned();
4645 let digest = digest.to_string();
4646 self.transaction(
4647 move |tx| {
4648 tx.prepare(
4649 "INSERT INTO t_component_source \
4650 (component_digest, frame_key, is_suffix, digest) \
4651 VALUES (:component_digest, :frame_key, :is_suffix, :digest) \
4652 ON CONFLICT (component_digest, frame_key, is_suffix) \
4653 DO UPDATE SET digest = excluded.digest",
4654 )?
4655 .execute(named_params! {
4656 ":component_digest": component_digest,
4657 ":frame_key": frame_key,
4658 ":is_suffix": is_suffix,
4659 ":digest": digest,
4660 })?;
4661 Ok(())
4662 },
4663 TxType::Other,
4664 "upsert_source_mapping",
4665 )
4666 .await
4667 }
4668
4669 #[instrument(skip_all)]
4670 async fn resolve_source_digest(
4671 &self,
4672 component_digest: &ComponentDigest,
4673 file: &str,
4674 ) -> Result<Option<ContentDigest>, DbErrorRead> {
4675 let component_digest = component_digest.clone();
4676 let file = file.to_owned();
4677 self.transaction(
4678 move |tx| {
4679 let mut stmt = tx.prepare(
4680 "SELECT digest \
4681 FROM t_component_source \
4682 WHERE component_digest = :component_digest \
4683 AND ( \
4684 (is_suffix = 0 AND frame_key = :file) \
4685 OR (is_suffix = 1 AND \
4686 substr(:file, length(:file) - length(frame_key) + 1) = frame_key) \
4687 )",
4688 )?;
4689 let rows: Vec<ContentDigest> = stmt
4690 .query_map(
4691 named_params! {
4692 ":component_digest": component_digest,
4693 ":file": file,
4694 },
4695 |row| row.get("digest"),
4696 )?
4697 .collect::<Result<_, _>>()?;
4698 match rows.len() {
4699 0 => Ok(None),
4700 1 => Ok(Some(rows.into_iter().next().unwrap())),
4701 _ => {
4702 warn!("Multiple suffix matches for '{file}', returning None");
4703 Ok(None)
4704 }
4705 }
4706 },
4707 TxType::Other,
4708 "resolve_source_digest",
4709 )
4710 .await
4711 }
4712
4713 #[instrument(skip_all)]
4714 async fn upsert_component_metadata(
4715 &self,
4716 records: Vec<ComponentMetadataRecord>,
4717 ) -> Result<(), DbErrorWrite> {
4718 self.transaction(
4719 move |tx| Self::upsert_component_metadata_tx(tx, &records),
4720 TxType::MultipleWrites,
4721 "upsert_component_metadata",
4722 )
4723 .await
4724 }
4725
4726 #[instrument(skip_all)]
4727 async fn insert_deployment_components(
4728 &self,
4729 deployment_id: DeploymentId,
4730 records: Vec<DeploymentComponentRecord>,
4731 ) -> Result<(), DbErrorWrite> {
4732 self.transaction(
4733 move |tx| Self::insert_deployment_components_tx(tx, deployment_id, &records),
4734 TxType::MultipleWrites,
4735 "insert_deployment_components",
4736 )
4737 .await
4738 }
4739
4740 #[instrument(skip_all)]
4741 async fn list_deployment_components(
4742 &self,
4743 deployment_id: DeploymentId,
4744 ) -> Result<Vec<DeploymentComponentDetail>, DbErrorRead> {
4745 self.transaction(
4746 move |tx| {
4747 let mut stmt = tx.prepare(
4748 "SELECT dc.component_name, dc.component_type, dc.component_digest, \
4749 cm.imports_json, cm.exports_json, cm.wit \
4750 FROM t_deployment_component dc \
4751 JOIN t_component_metadata cm ON dc.component_digest = cm.component_digest \
4752 WHERE dc.deployment_id = :deployment_id \
4753 ORDER BY dc.component_type, dc.component_name",
4754 )?;
4755 let rows = stmt
4756 .query_map(
4757 named_params! { ":deployment_id": deployment_id.to_string() },
4758 |row| {
4759 deployment_component_detail_from_row(row).map_err(|err| {
4760 rusqlite::Error::ToSqlConversionFailure(Box::new(err))
4761 })
4762 },
4763 )?
4764 .collect::<Result<Vec<_>, _>>()?;
4765 Ok(rows)
4766 },
4767 TxType::Other,
4768 "list_deployment_components",
4769 )
4770 .await
4771 }
4772
4773 #[instrument(skip_all)]
4774 async fn get_deployment_component_wit(
4775 &self,
4776 deployment_id: DeploymentId,
4777 component_digest: &ComponentDigest,
4778 ) -> Result<Option<String>, DbErrorRead> {
4779 let component_digest = component_digest.clone();
4780 self.transaction(
4781 move |tx| {
4782 tx.prepare(
4783 "SELECT cm.wit \
4784 FROM t_deployment_component dc \
4785 JOIN t_component_metadata cm ON dc.component_digest = cm.component_digest \
4786 WHERE dc.deployment_id = :deployment_id \
4787 AND dc.component_digest = :component_digest \
4788 LIMIT 1",
4789 )?
4790 .query_row(
4791 named_params! {
4792 ":deployment_id": deployment_id.to_string(),
4793 ":component_digest": component_digest,
4794 },
4795 |row| row.get(0),
4796 )
4797 .optional()
4798 .map_err(DbErrorRead::from)
4799 },
4800 TxType::Other,
4801 "get_deployment_component_wit",
4802 )
4803 .await
4804 }
4805
4806 #[instrument(skip(self))]
4807 async fn list_executions(
4808 &self,
4809 filter: ListExecutionsFilter,
4810 pagination: ExecutionListPagination,
4811 ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric> {
4812 self.transaction(
4813 move |tx| Self::list_executions(tx, &filter, &pagination),
4814 TxType::Other, "list_executions",
4816 )
4817 .await
4818 .map_err(to_generic_error)
4819 }
4820
4821 #[instrument(skip(self))]
4822 async fn list_execution_events(
4823 &self,
4824 execution_id: &ExecutionId,
4825 pagination: Pagination<VersionType>,
4826 include_backtrace_id: bool,
4827 ) -> Result<ListExecutionEventsResponse, DbErrorRead> {
4828 let execution_id = execution_id.clone();
4829 self.transaction(
4830 move |tx| {
4831 let events = Self::list_execution_events(
4832 tx,
4833 &execution_id,
4834 pagination,
4835 include_backtrace_id,
4836 )?;
4837 let max_version = Self::get_max_version(tx, &execution_id)?;
4838 Ok(ListExecutionEventsResponse {
4839 events,
4840 max_version,
4841 })
4842 },
4843 TxType::Other, "get",
4845 )
4846 .await
4847 }
4848
4849 #[instrument(skip(self))]
4850 async fn list_responses(
4851 &self,
4852 execution_id: &ExecutionId,
4853 pagination: Pagination<u32>,
4854 ) -> Result<ListResponsesResponse, DbErrorRead> {
4855 let execution_id = execution_id.clone();
4856 self.transaction(
4857 move |tx| {
4858 let responses = Self::list_responses(tx, &execution_id, Some(pagination))?;
4859 let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4860 Ok(ListResponsesResponse {
4861 responses,
4862 max_cursor,
4863 })
4864 },
4865 TxType::Other, "list_responses",
4867 )
4868 .await
4869 }
4870
4871 #[instrument(skip(self))]
4872 async fn list_execution_events_responses(
4873 &self,
4874 execution_id: &ExecutionId,
4875 req_since: &Version,
4876 req_max_length: VersionType,
4877 req_include_backtrace_id: bool,
4878 resp_pagination: Pagination<u32>,
4879 ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead> {
4880 let execution_id = execution_id.clone();
4881 let req_since = req_since.0;
4882 self.transaction(
4883 move |tx| {
4884 let combined_state = Self::get_combined_state(tx, &execution_id)?;
4885 let events = Self::list_execution_events(
4886 tx,
4887 &execution_id,
4888 Pagination::NewerThan {
4889 length: req_max_length
4890 .try_into()
4891 .expect("req_max_length fits in u16"),
4892 cursor: req_since,
4893 including_cursor: true,
4894 },
4895 req_include_backtrace_id,
4896 )?;
4897 let responses = Self::list_responses(tx, &execution_id, Some(resp_pagination))?;
4898 let max_version = Self::get_max_version(tx, &execution_id)?;
4899 let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4900 Ok(ExecutionWithStateRequestsResponses {
4901 execution_with_state: combined_state.execution_with_state,
4902 events,
4903 responses,
4904 max_version,
4905 max_cursor,
4906 })
4907 },
4908 TxType::Other, "list_execution_events_responses",
4910 )
4911 .await
4912 }
4913
4914 #[instrument(skip(self))]
4915 async fn upgrade_execution_component(
4916 &self,
4917 execution_id: &ExecutionId,
4918 old: &ComponentDigest,
4919 new: &ComponentDigest,
4920 reason: ComponentUpgradeReason,
4921 ) -> Result<(), DbErrorWrite> {
4922 let execution_id = execution_id.clone();
4923 let old = old.clone();
4924 let new = new.clone();
4925 self.transaction(
4926 move |tx| {
4927 Self::upgrade_execution_component_single_write(
4928 tx,
4929 &execution_id,
4930 &old,
4931 &new,
4932 reason.clone(),
4933 )
4934 },
4935 TxType::Other, "upgrade_execution_component",
4937 )
4938 .await
4939 }
4940
4941 #[instrument(skip(self))]
4942 async fn list_logs(
4943 &self,
4944 execution_id: &ExecutionId,
4945 show_derived: bool,
4946 filter: LogFilter,
4947 pagination: Pagination<LogCursor>,
4948 ) -> Result<ListLogsResponse, DbErrorRead> {
4949 let execution_id = execution_id.clone();
4950 self.transaction(
4951 move |tx| Self::list_logs_tx(tx, &execution_id, show_derived, &filter, &pagination),
4952 TxType::Other, "list_logs",
4954 )
4955 .await
4956 }
4957
4958 #[instrument(skip(self))]
4959 async fn list_deployment_states(
4960 &self,
4961 current_time: DateTime<Utc>,
4962 pagination: Pagination<Option<DeploymentId>>,
4963 include_deployment_toml: bool,
4964 execution_counts: DeploymentExecutionCounts,
4965 ) -> Result<Vec<DeploymentState>, DbErrorRead> {
4966 self.transaction(
4967 move |tx| {
4968 Self::list_deployment_states(
4969 tx,
4970 current_time,
4971 pagination,
4972 include_deployment_toml,
4973 execution_counts,
4974 )
4975 },
4976 TxType::Other, "list_deployment_states",
4978 )
4979 .await
4980 }
4981
4982 #[instrument(skip(self))]
4983 async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite> {
4984 self.transaction(
4985 move |tx| Self::insert_deployment_tx(tx, &record),
4986 TxType::MultipleWrites,
4987 "insert_deployment",
4988 )
4989 .await
4990 }
4991
4992 async fn insert_deployment_with_components(
4993 &self,
4994 record: DeploymentRecord,
4995 component_metadata: Vec<ComponentMetadataRecord>,
4996 deployment_components: Vec<DeploymentComponentRecord>,
4997 ) -> Result<(), DbErrorWrite> {
4998 let deployment_id = record.deployment_id;
4999 self.transaction(
5000 move |tx| {
5001 Self::insert_deployment_tx(tx, &record)?;
5003 Self::upsert_component_metadata_tx(tx, &component_metadata)?;
5004 Self::insert_deployment_components_tx(tx, deployment_id, &deployment_components)
5005 },
5006 TxType::MultipleWrites,
5007 "insert_deployment_with_components",
5008 )
5009 .await
5010 }
5011
5012 #[instrument(skip(self))]
5013 async fn missing_digests(
5014 &self,
5015 deployment_id: DeploymentId,
5016 ) -> Result<Vec<ContentDigest>, DbErrorRead> {
5017 self.transaction(
5018 move |tx| Self::missing_digests_tx(tx, deployment_id),
5019 TxType::Other,
5020 "missing_digests",
5021 )
5022 .await
5023 }
5024
5025 #[instrument(skip(self))]
5026 async fn list_deployment_files(
5027 &self,
5028 deployment_id: DeploymentId,
5029 ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead> {
5030 self.transaction(
5031 move |tx| Self::list_deployment_files_tx(tx, deployment_id),
5032 TxType::Other,
5033 "list_deployment_files",
5034 )
5035 .await
5036 }
5037
5038 #[instrument(skip(self))]
5039 async fn gc_orphan_files(&self) -> Result<u64, DbErrorWrite> {
5040 self.transaction(
5041 move |tx| {
5042 let deleted = tx
5043 .execute(
5044 "DELETE FROM t_file WHERE digest NOT IN \
5045 (SELECT digest FROM t_deployment_file \
5046 UNION SELECT digest FROM t_component_source)",
5047 [],
5048 )
5049 .map_err(RusqliteError::from)?;
5050 Ok(deleted as u64)
5051 },
5052 TxType::MultipleWrites,
5053 "gc_orphan_files",
5054 )
5055 .await
5056 }
5057
5058 #[instrument(skip(self))]
5059 async fn activate_deployment(
5060 &self,
5061 deployment_id: DeploymentId,
5062 now: DateTime<Utc>,
5063 ) -> Result<(), DbErrorWrite> {
5064 self.transaction(
5065 move |tx| Self::activate_deployment_tx(tx, deployment_id, now),
5066 TxType::MultipleWrites,
5067 "activate_deployment",
5068 )
5069 .await
5070 }
5071
5072 async fn enqueue_deployment(
5073 &self,
5074 deployment_id: DeploymentId,
5075 ) -> Result<EnqueueOutcome, DbErrorWrite> {
5076 self.transaction(
5077 move |tx| Self::enqueue_deployment_tx(tx, deployment_id),
5078 TxType::MultipleWrites,
5079 "enqueue_deployment",
5080 )
5081 .await
5082 }
5083
5084 #[instrument(skip(self))]
5085 async fn get_deployment(
5086 &self,
5087 deployment_id: DeploymentId,
5088 ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5089 self.transaction(
5090 move |tx| Self::get_deployment_tx(tx, deployment_id),
5091 TxType::Other,
5092 "get_deployment",
5093 )
5094 .await
5095 }
5096
5097 #[cfg(feature = "test")]
5098 #[instrument(skip(self))]
5099 async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5100 self.transaction(
5101 move |tx| Self::get_active_deployment_tx(tx),
5102 TxType::Other,
5103 "get_active_deployment",
5104 )
5105 .await
5106 }
5107
5108 #[instrument(skip(self))]
5109 async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5110 self.transaction(
5111 move |tx| {
5112 let Some(record) = tx
5113 .query_row(
5114 "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
5115 FROM t_deployment WHERE status IN ('enqueued', 'active') \
5116 ORDER BY CASE status WHEN 'enqueued' THEN 0 ELSE 1 END LIMIT 1",
5117 [],
5118 deployment_record_from_row,
5119 )
5120 .optional()
5121 .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
5122 else {
5123 return Ok(None);
5124 };
5125 Self::with_deployment_files_tx(tx, record).map(Some)
5126 },
5127 TxType::Other,
5128 "get_current_deployment",
5129 )
5130 .await
5131 }
5132
5133 #[instrument(skip(self))]
5134 async fn list_deployments(
5135 &self,
5136 pagination: Pagination<Option<DeploymentId>>,
5137 ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
5138 self.transaction(
5139 move |tx| Self::list_deployments_tx(tx, pagination),
5140 TxType::Other,
5141 "list_deployments",
5142 )
5143 .await
5144 }
5145
5146 #[instrument(skip(self))]
5147 async fn pause_execution(
5148 &self,
5149 execution_id: &ExecutionId,
5150 paused_at: DateTime<Utc>,
5151 ) -> Result<AppendResponse, DbErrorWrite> {
5152 let execution_id = execution_id.clone();
5153 self.transaction(
5154 move |tx| SqlitePool::pause_execution(tx, &execution_id, paused_at),
5155 TxType::MultipleWrites,
5156 "pause_execution",
5157 )
5158 .await
5159 }
5160
5161 #[instrument(skip(self))]
5162 async fn unpause_execution(
5163 &self,
5164 execution_id: &ExecutionId,
5165 unpaused_at: DateTime<Utc>,
5166 ) -> Result<AppendResponse, DbErrorWrite> {
5167 let execution_id = execution_id.clone();
5168 self.transaction(
5169 move |tx| SqlitePool::unpause_execution(tx, &execution_id, unpaused_at),
5170 TxType::MultipleWrites,
5171 "unpause_execution",
5172 )
5173 .await
5174 }
5175
5176 #[instrument(skip(self))]
5177 async fn pause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite> {
5178 let delay_id = delay_id.clone();
5179 self.transaction(
5180 move |tx| {
5181 let (execution_id, join_set_id) = delay_id.split_to_parts();
5182 let rows_modified = tx.execute(
5183 "UPDATE t_delay SET is_paused = 1 \
5184 WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id",
5185 named_params! {
5186 ":execution_id": execution_id.to_string(),
5187 ":join_set_id": join_set_id.to_string(),
5188 ":delay_id": delay_id.to_string(),
5189 },
5190 )?;
5191 if rows_modified == 0 {
5192 return Err(DbErrorWrite::NotFound);
5193 }
5194 Ok(())
5195 },
5196 TxType::Other,
5197 "pause_delay",
5198 )
5199 .await
5200 }
5201
5202 #[instrument(skip(self))]
5203 async fn unpause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite> {
5204 let delay_id = delay_id.clone();
5205 self.transaction(
5206 move |tx| {
5207 let (execution_id, join_set_id) = delay_id.split_to_parts();
5208 let rows_modified = tx.execute(
5209 "UPDATE t_delay SET is_paused = 0 \
5210 WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id",
5211 named_params! {
5212 ":execution_id": execution_id.to_string(),
5213 ":join_set_id": join_set_id.to_string(),
5214 ":delay_id": delay_id.to_string(),
5215 },
5216 )?;
5217 if rows_modified == 0 {
5218 return Err(DbErrorWrite::NotFound);
5219 }
5220 Ok(())
5221 },
5222 TxType::Other,
5223 "unpause_delay",
5224 )
5225 .await
5226 }
5227}
5228
5229#[async_trait]
5230impl Cas for SqlitePool {
5231 async fn read_blob(&self, digest: &ContentDigest) -> Result<Option<Vec<u8>>, CasError> {
5232 let digest = digest.clone();
5233 self.transaction(
5234 move |tx| Self::get_file_tx(tx, &digest),
5235 TxType::Other,
5236 "cas_read_blob",
5237 )
5238 .await
5239 .map_err(|err| CasError::Uncategorized(err.to_string()))
5240 }
5241
5242 async fn write_blob(&self, content: &[u8]) -> Result<ContentDigest, CasError> {
5243 let digest = Self::compute_file_digest(content);
5244 let content = content.to_vec();
5245 {
5246 let digest = digest.clone();
5247 self.transaction(
5248 move |tx| Self::upload_file_tx(tx, &digest, &content),
5249 TxType::MultipleWrites,
5250 "cas_write_blob",
5251 )
5252 .await
5253 .map_err(|err| CasError::Uncategorized(err.to_string()))?;
5254 }
5255 Ok(digest)
5256 }
5257
5258 async fn contains_blob(&self, digest: &ContentDigest) -> Result<bool, CasError> {
5259 Ok(self.read_blob(digest).await?.is_some())
5260 }
5261}
5262
5263#[async_trait]
5264impl DbConnection for SqlitePool {
5265 #[instrument(level = Level::DEBUG, skip_all, fields(execution_id = %req.execution_id))]
5266 async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite> {
5267 debug!("create");
5268 trace!(?req, "create");
5269 let created_at = req.created_at;
5270 let (version, notifier) = self
5271 .transaction(
5272 move |tx| Self::create_inner(tx, req.clone()),
5273 TxType::MultipleWrites,
5274 "create",
5275 )
5276 .await?;
5277 self.notify_all(vec![notifier], created_at);
5278 Ok(version)
5279 }
5280
5281 #[instrument(level = Level::DEBUG, skip(self))]
5282 async fn get(
5283 &self,
5284 execution_id: &ExecutionId,
5285 ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
5286 trace!("get");
5287 let execution_id = execution_id.clone();
5288 self.transaction(
5289 move |tx| Self::get(tx, &execution_id),
5290 TxType::Other, "get",
5292 )
5293 .await
5294 }
5295
5296 #[instrument(level = Level::DEBUG, skip(self))]
5297 async fn get_cancelling(&self, batch_size: u32) -> Result<Vec<ExecutionId>, DbErrorRead> {
5298 self.transaction(
5299 move |tx| {
5300 let mut stmt = tx.prepare(
5301 "SELECT execution_id FROM t_state WHERE lifecycle = :lifecycle \
5302 ORDER BY created_at LIMIT :batch_size",
5303 )?;
5304 let rows = stmt
5305 .query_map(
5306 named_params! {
5307 ":lifecycle": LIFECYCLE_CANCELLING,
5308 ":batch_size": batch_size,
5309 },
5310 |row| row.get::<_, String>("execution_id"),
5311 )?
5312 .collect::<Result<Vec<_>, _>>()?;
5313 rows.into_iter()
5314 .map(|id| {
5315 ExecutionId::from_str(&id)
5316 .map_err(|_| consistency_rusqlite("invalid t_state.execution_id"))
5317 .map_err(DbErrorRead::from)
5318 })
5319 .collect()
5320 },
5321 TxType::Other, "get_cancelling",
5323 )
5324 .await
5325 }
5326
5327 #[instrument(level = Level::DEBUG, skip(self, batch))]
5328 async fn append_batch(
5329 &self,
5330 current_time: DateTime<Utc>,
5331 batch: Vec<AppendRequest>,
5332 execution_id: ExecutionId,
5333 version: Version,
5334 ) -> Result<AppendBatchResponse, DbErrorWrite> {
5335 debug!("append_batch");
5336 trace!(?batch, "append_batch");
5337 assert!(!batch.is_empty(), "Empty batch request");
5338
5339 let (version, notifier) = self
5340 .transaction(
5341 move |tx| {
5342 let mut version = version.clone();
5343 let mut notifier = None;
5344 for append_request in &batch {
5345 let (v, n) =
5346 Self::append(tx, &execution_id, append_request.clone(), version)?;
5347 version = v;
5348 notifier = Some(n);
5349 }
5350 Ok::<_, DbErrorWrite>((
5351 version,
5352 notifier.expect("checked that the batch is not empty"),
5353 ))
5354 },
5355 TxType::MultipleWrites,
5356 "append_batch",
5357 )
5358 .await?;
5359
5360 self.notify_all(vec![notifier], current_time);
5361 Ok(version)
5362 }
5363
5364 #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %version))]
5365 async fn append_batch_create_new_execution(
5366 &self,
5367 current_time: DateTime<Utc>,
5368 batch: Vec<AppendRequest>,
5369 execution_id: ExecutionId,
5370 version: Version,
5371 child_req: Vec<CreateRequest>,
5372 backtraces: Vec<BacktraceInfo>,
5373 ) -> Result<AppendBatchResponse, DbErrorWrite> {
5374 debug!("append_batch_create_new_execution");
5375 trace!(?batch, ?child_req, "append_batch_create_new_execution");
5376 assert!(!batch.is_empty(), "Empty batch request");
5377
5378 let (version, notifiers) = self
5379 .transaction(
5380 move |tx| {
5381 let mut notifier = None;
5382 let mut version = version.clone();
5383 for append_request in &batch {
5384 let (v, n) =
5385 Self::append(tx, &execution_id, append_request.clone(), version)?;
5386 version = v;
5387 notifier = Some(n);
5388 }
5389 let mut notifiers = Vec::new();
5390 notifiers.push(notifier.expect("checked that the batch is not empty"));
5391
5392 for child_req in &child_req {
5393 let (_, notifier) = Self::create_inner(tx, child_req.clone())?;
5394 notifiers.push(notifier);
5395 }
5396 Ok::<_, DbErrorWrite>((version, notifiers))
5397 },
5398 TxType::MultipleWrites,
5399 "append_batch_create_new_execution_inner",
5400 )
5401 .await?;
5402 self.notify_all(notifiers, current_time);
5403 self.transaction_fire_forget(
5404 move |tx| {
5405 for backtrace in &backtraces {
5406 Self::append_backtrace(tx, backtrace)?;
5407 }
5408 Ok::<_, DbErrorWrite>(())
5409 },
5410 "append_batch_create_new_execution_append_backtrace",
5411 )
5412 .await;
5413 Ok(version)
5414 }
5415
5416 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
5420 async fn subscribe_to_next_responses(
5421 &self,
5422 execution_id: &ExecutionId,
5423 last_response: ResponseCursor,
5424 timeout_fut: Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>,
5425 ) -> Result<Vec<ResponseWithCursor>, DbErrorReadWithTimeout> {
5426 debug!("next_responses");
5427 let unique_tag: u64 = rand::random();
5428 let execution_id = execution_id.clone();
5429
5430 let cleanup = || {
5431 let mut guard = self.0.response_subscribers.lock().unwrap();
5432 match guard.remove(&execution_id) {
5433 Some((_, tag)) if tag == unique_tag => {} Some(other) => {
5435 guard.insert(execution_id.clone(), other);
5437 }
5438 None => {} }
5440 };
5441
5442 let response_subscribers = self.0.response_subscribers.clone();
5443 let resp_or_receiver = {
5444 let execution_id = execution_id.clone();
5445 self.transaction(
5446 move |tx| {
5447 let responses = Self::get_responses_after(tx, &execution_id, last_response)?;
5448 if responses.is_empty() {
5449 let (sender, receiver) = oneshot::channel();
5451 response_subscribers
5452 .lock()
5453 .unwrap()
5454 .insert(execution_id.clone(), (sender, unique_tag));
5455 Ok::<_, DbErrorReadWithTimeout>(itertools::Either::Right(receiver))
5456 } else {
5457 Ok(itertools::Either::Left(responses))
5458 }
5459 },
5460 TxType::Other, "subscribe_to_next_responses",
5462 )
5463 .await
5464 }
5465 .inspect_err(|_| {
5466 cleanup();
5467 })?;
5468 match resp_or_receiver {
5469 itertools::Either::Left(resp) => Ok(resp), itertools::Either::Right(receiver) => {
5471 let woken = tokio::select! {
5472 resp = receiver => match resp {
5473 Ok(()) => Ok(()),
5474 Err(_) => Err(DbErrorReadWithTimeout::from(DbErrorGeneric::Close)),
5475 },
5476 outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
5477 };
5478 cleanup();
5479 woken?;
5480 let execution_id = execution_id.clone();
5482 self.transaction(
5483 move |tx| {
5484 Self::get_responses_after(tx, &execution_id, last_response)
5485 .map_err(DbErrorReadWithTimeout::from)
5486 },
5487 TxType::Other, "subscribe_to_next_responses_refetch",
5489 )
5490 .await
5491 }
5492 }
5493 }
5494
5495 #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
5497 async fn wait_for_finished_result(
5498 &self,
5499 execution_id: &ExecutionId,
5500 timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
5501 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
5502 let unique_tag: u64 = rand::random();
5503 let execution_id = execution_id.clone();
5504 let execution_finished_subscription = self.0.execution_finished_subscribers.clone();
5505
5506 let cleanup = || {
5507 let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
5508 if let Some(subscribers) = guard.get_mut(&execution_id) {
5509 subscribers.remove(&unique_tag);
5510 }
5511 };
5512
5513 let resp_or_receiver = {
5514 let execution_id = execution_id.clone();
5515 self.transaction(move |tx| {
5516 let pending_state =
5517 Self::get_combined_state(tx, &execution_id)?.execution_with_state.pending_state;
5518 if let PendingState::Finished(finished) = pending_state {
5519 let event =
5520 Self::get_execution_event(tx, &execution_id, finished.version)?;
5521 if let ExecutionRequest::Finished { retval, ..} = event.event {
5522 Ok(itertools::Either::Left(retval))
5523 } else {
5524 error!("Mismatch, expected Finished row: {event:?} based on t_state {finished}");
5525 Err(DbErrorReadWithTimeout::from(consistency_db_err(
5526 "cannot get finished event based on t_state version"
5527 )))
5528 }
5529 } else {
5530 let (sender, receiver) = oneshot::channel();
5534 let mut guard = execution_finished_subscription.lock().unwrap();
5535 guard.entry(execution_id.clone()).or_default().insert(unique_tag, sender);
5536 Ok(itertools::Either::Right(receiver))
5537 }
5538 },
5539 TxType::Other, "wait_for_finished_result")
5541 .await
5542 }
5543 .inspect_err(|_| {
5544 cleanup();
5548 })?;
5549
5550 let timeout_fut = timeout_fut.unwrap_or_else(|| Box::pin(std::future::pending()));
5551 match resp_or_receiver {
5552 itertools::Either::Left(resp) => Ok(resp), itertools::Either::Right(receiver) => {
5554 let res = tokio::select! {
5555 resp = receiver => {
5556 match resp {
5557 Ok(retval) => Ok(retval),
5558 Err(_recv_err) => Err(DbErrorGeneric::Close.into())
5559 }
5560 }
5561 outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
5562 };
5563 cleanup();
5564 res
5565 }
5566 }
5567 }
5568
5569 #[instrument(level = Level::DEBUG, skip_all, fields(%join_set_id, %execution_id))]
5570 async fn append_delay_response(
5571 &self,
5572 created_at: DateTime<Utc>,
5573 execution_id: ExecutionId,
5574 join_set_id: JoinSetId,
5575 delay_id: DelayId,
5576 result: Result<(), ()>,
5577 ) -> Result<AppendDelayResponseOutcome, DbErrorWrite> {
5578 debug!("append_delay_response");
5579 let event = JoinSetResponseEventOuter {
5580 created_at,
5581 event: JoinSetResponseEvent {
5582 join_set_id,
5583 event: JoinSetResponse::DelayFinished {
5584 delay_id: delay_id.clone(),
5585 result,
5586 },
5587 },
5588 };
5589 let res = self
5590 .transaction(
5591 {
5592 let execution_id = execution_id.clone();
5593 move |tx| Self::append_response(tx, &execution_id, event.clone())
5594 },
5595 TxType::MultipleWrites,
5596 "append_delay_response",
5597 )
5598 .await;
5599 match res {
5600 Ok(notifier) => {
5601 self.notify_all(vec![notifier], created_at);
5602 Ok(AppendDelayResponseOutcome::Success)
5603 }
5604 Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)) => {
5605 let delay_success = self
5606 .transaction(
5607 move |tx| Self::get_delay_response(tx, &execution_id, &delay_id),
5608 TxType::Other, "get_delay_response",
5610 )
5611 .await?;
5612 match delay_success {
5613 Some(true) => Ok(AppendDelayResponseOutcome::AlreadyFinished),
5614 Some(false) => Ok(AppendDelayResponseOutcome::AlreadyCancelled),
5615 None => Err(DbErrorWrite::Generic(DbErrorGeneric::Uncategorized {
5616 reason: "insert failed yet select did not find the response".into(),
5617 context: SpanTrace::capture(),
5618 source: None,
5619 loc: Location::caller(),
5620 })),
5621 }
5622 }
5623 Err(err) => Err(err),
5624 }
5625 }
5626
5627 #[instrument(level = Level::DEBUG, skip_all)]
5628 async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite> {
5629 trace!("append_backtrace");
5630 self.transaction_fire_forget(
5631 move |tx| Self::append_backtrace(tx, &append).map(drop),
5632 "append_backtrace",
5633 )
5634 .await;
5635 Ok(())
5636 }
5637
5638 #[instrument(level = Level::DEBUG, skip_all)]
5639 async fn append_backtrace_batch(
5640 &self,
5641 batch: Vec<BacktraceInfo>,
5642 ) -> Result<usize, DbErrorWrite> {
5643 trace!("append_backtrace_batch");
5644 self.transaction(
5645 move |tx| {
5646 let mut inserted = 0;
5647 for append in &batch {
5648 inserted += Self::append_backtrace(tx, append)?;
5649 }
5650 Ok::<_, DbErrorWrite>(inserted)
5651 },
5652 TxType::MultipleWrites,
5653 "append_backtrace_batch",
5654 )
5655 .await
5656 }
5657
5658 #[instrument(level = Level::DEBUG, skip_all)]
5659 async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite> {
5660 trace!("append_log");
5661 self.transaction_fire_forget(move |tx| Self::append_log(tx, &row), "append_log")
5662 .await;
5663 Ok(())
5664 }
5665
5666 #[instrument(level = Level::DEBUG, skip_all)]
5667 async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite> {
5668 trace!("append_log_batch");
5669 let batch = Vec::from(batch);
5670 self.transaction_fire_forget(
5671 move |tx| {
5672 for row in &batch {
5673 Self::append_log(tx, row)?;
5674 }
5675 Ok::<_, DbErrorWrite>(())
5676 },
5677 "append_log_batch",
5678 )
5679 .await;
5680 Ok(())
5681 }
5682
5683 #[instrument(level = Level::TRACE, skip(self))]
5685 async fn get_expired_timers(
5686 &self,
5687 at: DateTime<Utc>,
5688 ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric> {
5689 self.transaction(
5690 move |conn| {
5691 let mut expired_timers = conn.prepare(
5692 "SELECT execution_id, join_set_id, delay_id FROM t_delay WHERE expires_at <= :at AND NOT is_paused",
5693 )?
5694 .query_map(
5695 named_params! {
5696 ":at": at,
5697 },
5698 |row| {
5699 let execution_id = row.get("execution_id")?;
5700 let join_set_id = row.get::<_, JoinSetId>("join_set_id")?;
5701 let delay_id = row.get::<_, DelayId>("delay_id")?;
5702 let delay = ExpiredDelay { execution_id, join_set_id, delay_id };
5703 Ok(ExpiredTimer::Delay(delay))
5704 },
5705 )?
5706 .collect::<Result<Vec<_>, _>>()?;
5707 let expired = conn.prepare(&format!(r#"
5709 SELECT execution_id, last_lock_version, corresponding_version, intermittent_event_count, max_retries, retry_exp_backoff_millis,
5710 executor_id, run_id, lifecycle
5711 FROM t_state
5712 WHERE pending_expires_finished <= :at AND state = "{STATE_LOCKED}"
5713 "#
5714 )
5715 )?
5716 .query_map(
5717 named_params! {
5718 ":at": at,
5719 },
5720 |row| {
5721 let execution_id = row.get("execution_id")?;
5722 let lifecycle: String = row.get("lifecycle")?;
5723 if lifecycle != LIFECYCLE_ACTIVE {
5724 error!(%execution_id, %lifecycle, "encountered invalid non-active locked execution while scanning expired locks");
5725 return Ok(None);
5726 }
5727 let locked_at_version = Version::new(row.get("last_lock_version")?);
5728 let next_version = Version::new(row.get("corresponding_version")?).increment();
5729 let intermittent_event_count = row.get("intermittent_event_count")?;
5730 let max_retries = row.get("max_retries")?;
5731 let retry_exp_backoff_millis = u64::from(row.get::<_, u32>("retry_exp_backoff_millis")?);
5732 let executor_id = row.get("executor_id")?;
5733 let run_id = row.get("run_id")?;
5734 let lock = ExpiredLock {
5735 execution_id,
5736 locked_at_version,
5737 next_version,
5738 intermittent_event_count,
5739 max_retries,
5740 retry_exp_backoff: Duration::from_millis(retry_exp_backoff_millis),
5741 locked_by: LockedBy { executor_id, run_id },
5742 };
5743 Ok(Some(ExpiredTimer::Lock(lock)))
5744 }
5745 )?
5746 .collect::<Result<Vec<_>, _>>()?;
5747 expired_timers.extend(expired.into_iter().flatten());
5748 if !expired_timers.is_empty() {
5749 debug!("get_expired_timers found {expired_timers:?}");
5750 }
5751 Ok(expired_timers)
5752 },
5753 TxType::Other, "get_expired_timers"
5755 )
5756 .await
5757 .map_err(to_generic_error)
5758 }
5759
5760 async fn get_execution_event(
5761 &self,
5762 execution_id: &ExecutionId,
5763 version: &Version,
5764 ) -> Result<ExecutionEvent, DbErrorRead> {
5765 let version = version.0;
5766 let execution_id = execution_id.clone();
5767 self.transaction(
5768 move |tx| Self::get_execution_event(tx, &execution_id, version),
5769 TxType::Other, "get_execution_event",
5771 )
5772 .await
5773 }
5774
5775 #[instrument(level = Level::DEBUG, skip_all)]
5776 async fn upsert_stub_response(
5777 &self,
5778 execution_id: ExecutionIdDerived,
5779 version: Version,
5780 req: AppendRequest,
5781 response: AppendResponseToExecution,
5782 current_time: DateTime<Utc>,
5783 ) -> Result<(), DbErrorStubResponse> {
5784 debug!("upsert_stub_response");
5785 #[cfg(debug_assertions)]
5786 {
5787 let (expected_parent, expected_join_set) = execution_id.split_to_parts();
5788 debug_assert_eq!(expected_parent, response.parent_execution_id);
5789 debug_assert_eq!(expected_join_set, response.join_set_id);
5790 debug_assert_eq!(execution_id, response.child_execution_id);
5791 }
5792 let execution_id = ExecutionId::Derived(execution_id);
5793 let expected_retval = response.result.clone();
5794 let notifiers = self
5795 .transaction(
5796 move |tx| {
5797 let version_raw = version.0;
5798 match Self::append(tx, &execution_id, req.clone(), version.clone()) {
5799 Ok((_next_version, notifier_of_child)) => {
5800 let pending_at_parent = Self::append_response(
5801 tx,
5802 &response.parent_execution_id,
5803 JoinSetResponseEventOuter {
5804 created_at: response.created_at,
5805 event: JoinSetResponseEvent {
5806 join_set_id: response.join_set_id.clone(),
5807 event: JoinSetResponse::ChildExecutionFinished {
5808 child_execution_id: response.child_execution_id.clone(),
5809 finished_version: response.finished_version.clone(),
5810 result: response.result.clone(),
5811 },
5812 },
5813 },
5814 )
5815 .map_err(DbErrorStubResponse::Write)?;
5816 Ok::<_, DbErrorStubResponse>(Some(vec![
5817 notifier_of_child,
5818 pending_at_parent,
5819 ]))
5820 }
5821 Err(DbErrorWrite::NonRetriable(
5822 DbErrorWriteNonRetriable::AlreadyFinished,
5823 )) => {
5824 let found = Self::get_execution_event(tx, &execution_id, version_raw)
5826 .map_err(|_| DbErrorStubResponse::StubConflict)?;
5827 match found.event {
5828 ExecutionRequest::Finished { retval, .. }
5829 if retval == expected_retval =>
5830 {
5831 Ok(None)
5832 }
5833 _ => Err(DbErrorStubResponse::StubConflict),
5834 }
5835 }
5836 Err(other) => Err(DbErrorStubResponse::Write(other)),
5837 }
5838 },
5839 TxType::MultipleWrites,
5840 "upsert_stub_response",
5841 )
5842 .await?;
5843 if let Some(notifiers) = notifiers {
5844 self.notify_all(notifiers, current_time);
5845 }
5846 Ok(())
5847 }
5848
5849 async fn get_pending_state(
5850 &self,
5851 execution_id: &ExecutionId,
5852 ) -> Result<ExecutionWithState, DbErrorRead> {
5853 let execution_id = execution_id.clone();
5854 Ok(self
5855 .transaction(
5856 move |tx| Self::get_combined_state(tx, &execution_id),
5857 TxType::Other, "get_pending_state",
5859 )
5860 .await?
5861 .execution_with_state)
5862 }
5863}
5864
5865#[cfg(feature = "test")]
5866#[async_trait]
5867impl concepts::storage::DbConnectionTest for SqlitePool {
5868 #[instrument(level = Level::DEBUG, skip(self, response_event), fields(join_set_id = %response_event.join_set_id))]
5869 async fn append_response(
5870 &self,
5871 created_at: DateTime<Utc>,
5872 execution_id: ExecutionId,
5873 response_event: JoinSetResponseEvent,
5874 ) -> Result<(), DbErrorWrite> {
5875 debug!("append_response");
5876 let event = JoinSetResponseEventOuter {
5877 created_at,
5878 event: response_event,
5879 };
5880 let notifier = self
5881 .transaction(
5882 move |tx| Self::append_response(tx, &execution_id, event.clone()),
5883 TxType::Other, "append_response",
5885 )
5886 .await?;
5887 self.notify_all(vec![notifier], created_at);
5888 Ok(())
5889 }
5890}
5891
5892#[cfg(any(test, feature = "tempfile"))]
5893pub mod tempfile {
5894 use super::{SqliteConfig, SqlitePool};
5895 use tempfile::NamedTempFile;
5896
5897 pub async fn sqlite_pool() -> (SqlitePool, Option<NamedTempFile>) {
5898 if let Ok(path) = std::env::var("SQLITE_FILE") {
5899 (
5900 SqlitePool::new(path, SqliteConfig::default())
5901 .await
5902 .unwrap(),
5903 None,
5904 )
5905 } else {
5906 let file = NamedTempFile::new().unwrap();
5907 let path = file.path();
5908 (
5909 SqlitePool::new(path, SqliteConfig::default())
5910 .await
5911 .unwrap(),
5912 Some(file),
5913 )
5914 }
5915 }
5916}
5917
5918#[cfg(test)]
5919mod tests {
5920 use crate::sqlite_dao::{SqlitePool, TxType, tempfile::sqlite_pool};
5921 use assert_matches::assert_matches;
5922 use chrono::DateTime;
5923 use concepts::{
5924 ComponentId, FunctionFqn, Params,
5925 prefixed_ulid::{DEPLOYMENT_ID_DUMMY, EXECUTION_ID_DUMMY},
5926 storage::{CreateRequest, DbErrorWrite, DbErrorWriteNonRetriable, DbPoolCloseable},
5927 };
5928 use rusqlite::named_params;
5929
5930 const SOME_FFQN: FunctionFqn = FunctionFqn::new_static("ns:pkg/ifc", "fn");
5931
5932 #[tokio::test]
5933 async fn failing_ltx_should_be_rolled_back() -> Result<(), DbErrorWrite> {
5934 let created_at = DateTime::from_timestamp_nanos(0);
5935 let (pool, _guard) = sqlite_pool().await;
5936 pool.transaction(
5937 move |tx| {
5938 let req = CreateRequest {
5939 created_at,
5940 execution_id: EXECUTION_ID_DUMMY,
5941 ffqn: SOME_FFQN,
5942 params: Params::empty(),
5943 parent: None,
5944 metadata: concepts::ExecutionMetadata::empty(),
5945 scheduled_at: created_at,
5946 component_id: ComponentId::dummy_activity(),
5947 deployment_id: DEPLOYMENT_ID_DUMMY,
5948 scheduled_by: None,
5949 paused: false,
5950 };
5951 SqlitePool::create_inner(tx, req)?;
5952 SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at)?;
5953 Ok::<_, DbErrorWrite>(())
5954 },
5955 TxType::MultipleWrites,
5956 "create_inner + pause_execution",
5957 )
5958 .await?;
5959
5960 let err = pool
5962 .transaction(
5963 move |tx| SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at),
5964 TxType::MultipleWrites,
5965 "pause_execution",
5966 )
5967 .await
5968 .unwrap_err();
5969 let reason = assert_matches!(err, DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::IllegalState { reason, .. }) => reason);
5970 assert_eq!("cannot pause, execution is already paused", reason.as_ref());
5971
5972 let events = pool.transaction(
5973 move |tx| {
5974 let events =
5975 tx.prepare(
5976 "SELECT created_at, json_value, version FROM t_execution_log WHERE execution_id = :execution_id",
5977 )?
5978 .query_map(
5979 named_params! {
5980 ":execution_id": EXECUTION_ID_DUMMY.to_string(),
5981 },
5982 SqlitePool::map_t_execution_log_row,
5983 )
5984 .map_err(DbErrorWrite::from)?
5985 .collect::<Result<Vec<_>, _>>()?;
5986
5987 Ok::<_, DbErrorWrite>(events)
5988 },
5989 TxType::Other, "get_log",
5991 )
5992 .await?;
5993 assert_eq!(2, events.len());
5994 pool.close().await;
5995 Ok(())
5996 }
5997}