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