1use crate::ComponentId;
2use crate::ComponentRetryConfig;
3use crate::ComponentType;
4use crate::ExecutionFailureKind;
5use crate::ExecutionId;
6use crate::ExecutionMetadata;
7use crate::FinishedExecutionFailure;
8use crate::FunctionExtension;
9use crate::FunctionFqn;
10use crate::FunctionMetadata;
11use crate::JoinSetId;
12use crate::Params;
13use crate::StrVariant;
14use crate::SupportedFunctionReturnValue;
15use crate::component_id::ComponentDigest;
16use crate::prefixed_ulid::DelayId;
17use crate::prefixed_ulid::DeploymentId;
18use crate::prefixed_ulid::ExecutionIdDerived;
19use crate::prefixed_ulid::ExecutorId;
20use crate::prefixed_ulid::RunId;
21use assert_matches::assert_matches;
22use async_trait::async_trait;
23use chrono::TimeDelta;
24use chrono::{DateTime, Utc};
25use http_client_trace::HttpClientTrace;
26use serde::Deserialize;
27use serde::Serialize;
28use std::fmt::Debug;
29use std::fmt::Display;
30use std::panic::Location;
31use std::pin::Pin;
32use std::sync::Arc;
33use std::time::Duration;
34use tracing::debug;
35use tracing::instrument;
36use tracing_error::SpanTrace;
37
38pub const STATE_PENDING_AT: &str = "pending_at";
40pub const STATE_BLOCKED_BY_JOIN_SET: &str = "blocked_by_join_set";
41pub const STATE_LOCKED: &str = "locked";
42pub const STATE_FINISHED: &str = "finished";
43pub const HISTORY_EVENT_TYPE_JOIN_NEXT: &str = "join_next"; #[derive(Debug, PartialEq, Eq, Clone)]
46pub struct ExecutionLog {
47 pub execution_id: ExecutionId,
48 pub events: Vec<ExecutionEvent>,
49 pub responses: Vec<ResponseWithCursor>,
50 pub next_version: Version, pub pending_state: PendingState, pub component_digest: ComponentDigest, pub component_type: ComponentType,
54 pub deployment_id: DeploymentId, }
56
57impl ExecutionLog {
58 #[must_use]
61 pub fn can_be_retried_after(
62 temporary_event_count: u32,
63 max_retries: Option<u32>,
64 retry_exp_backoff: Duration,
65 ) -> Option<Duration> {
66 if temporary_event_count <= max_retries.unwrap_or(u32::MAX) {
68 let duration = retry_exp_backoff * 2_u32.saturating_pow(temporary_event_count - 1);
70 Some(duration)
71 } else {
72 None
73 }
74 }
75
76 #[must_use]
77 pub fn compute_retry_duration_when_retrying_forever(
78 temporary_event_count: u32,
79 retry_exp_backoff: Duration,
80 ) -> Duration {
81 Self::can_be_retried_after(temporary_event_count, None, retry_exp_backoff)
82 .expect("`max_retries` set to MAX must never return None")
83 }
84
85 #[must_use]
86 pub fn get_create_request(&self) -> CreateRequest {
87 assert_matches!(self.events.first().cloned(), Some(ExecutionEvent {
88 event:ExecutionRequest::Created{
89 ffqn,params,parent,scheduled_at,component_id,deployment_id,metadata,scheduled_by},
90 created_at, .. }) => CreateRequest { created_at, execution_id:
91 self.execution_id.clone(), ffqn, params, parent, scheduled_at,
92 component_id, deployment_id, metadata, scheduled_by, paused: false })
93 }
94
95 #[must_use]
96 pub fn ffqn(&self) -> &FunctionFqn {
97 assert_matches!(self.events.first(), Some(ExecutionEvent {
98 event: ExecutionRequest::Created { ffqn, .. },
99 ..
100 }) => ffqn)
101 }
102
103 #[must_use]
104 pub fn params(&self) -> &Params {
105 assert_matches!(self.events.first(), Some(ExecutionEvent {
106 event: ExecutionRequest::Created { params, .. },
107 ..
108 }) => params)
109 }
110
111 #[must_use]
112 pub fn parent(&self) -> Option<(ExecutionId, JoinSetId)> {
113 assert_matches!(self.events.first(), Some(ExecutionEvent {
114 event: ExecutionRequest::Created { parent, .. },
115 ..
116 }) => parent.clone())
117 }
118
119 #[must_use]
120 pub fn last_event(&self) -> &ExecutionEvent {
121 self.events.last().expect("must contain at least one event")
122 }
123
124 #[must_use]
125 pub fn is_finished(&self) -> bool {
126 matches!(
127 self.events.last(),
128 Some(ExecutionEvent {
129 event: ExecutionRequest::Finished { .. },
130 ..
131 })
132 )
133 }
134
135 #[must_use]
136 pub fn as_finished_result(&self) -> Option<SupportedFunctionReturnValue> {
137 if let ExecutionEvent {
138 event: ExecutionRequest::Finished { retval: result, .. },
139 ..
140 } = self.events.last().expect("must contain at least one event")
141 {
142 Some(result.clone())
143 } else {
144 None
145 }
146 }
147
148 pub fn event_history(&self) -> impl Iterator<Item = (HistoryEvent, Version)> + '_ {
149 self.events.iter().filter_map(|event| {
150 if let ExecutionRequest::HistoryEvent { event: eh, .. } = &event.event {
151 Some((eh.clone(), event.version.clone()))
152 } else {
153 None
154 }
155 })
156 }
157
158 #[cfg(feature = "test")]
159 #[must_use]
160 pub fn find_join_set_request(&self, join_set_id: &JoinSetId) -> Option<&JoinSetRequest> {
161 self.events
162 .iter()
163 .find_map(move |event| match &event.event {
164 ExecutionRequest::HistoryEvent {
165 event:
166 HistoryEvent::JoinSetRequest {
167 join_set_id: found,
168 request,
169 },
170 ..
171 } if *join_set_id == *found => Some(request),
172 _ => None,
173 })
174 }
175}
176
177pub type VersionType = u32;
178#[derive(
179 Debug,
180 Default,
181 Clone,
182 PartialEq,
183 PartialOrd,
184 Ord,
185 Eq,
186 Hash,
187 derive_more::Display,
188 derive_more::Into,
189 serde::Serialize,
190 serde::Deserialize,
191 schemars::JsonSchema,
192)]
193#[serde(transparent)]
194#[schemars(transparent)]
195pub struct Version(pub VersionType);
196impl Version {
197 #[must_use]
198 pub fn new(arg: VersionType) -> Version {
199 Version(arg)
200 }
201
202 #[must_use]
203 pub fn increment(&self) -> Version {
204 Version(self.0 + 1)
205 }
206}
207impl TryFrom<i64> for Version {
208 type Error = VersionParseError;
209 fn try_from(value: i64) -> Result<Self, Self::Error> {
210 VersionType::try_from(value)
211 .map(Version::new)
212 .map_err(|_| VersionParseError)
213 }
214}
215impl From<Version> for usize {
216 fn from(value: Version) -> Self {
217 usize::try_from(value.0).expect("16 bit systems are unsupported")
218 }
219}
220impl From<&Version> for usize {
221 fn from(value: &Version) -> Self {
222 usize::try_from(value.0).expect("16 bit systems are unsupported")
223 }
224}
225
226#[derive(Debug, thiserror::Error)]
227#[error("version must be u32")]
228pub struct VersionParseError;
229
230#[derive(
231 Clone,
232 Debug,
233 derive_more::Display,
234 PartialEq,
235 Eq,
236 serde::Serialize,
237 serde::Deserialize,
238 schemars::JsonSchema,
239)]
240#[display("{event}")]
241pub struct ExecutionEvent {
242 pub created_at: DateTime<Utc>,
243 pub event: ExecutionRequest,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub backtrace_id: Option<Version>,
246 pub version: Version,
247}
248
249#[derive(
250 Debug,
251 Clone,
252 Copy,
253 PartialEq,
254 Eq,
255 derive_more::Display,
256 derive_more::Into,
257 Serialize, schemars::JsonSchema,
259)]
260pub struct ResponseCursor(pub u32);
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize , schemars::JsonSchema)]
263pub struct ResponseWithCursor {
264 pub event: JoinSetResponseEventOuter,
265 pub cursor: ResponseCursor,
266}
267
268#[derive(Debug)]
269pub struct ListExecutionEventsResponse {
270 pub events: Vec<ExecutionEvent>,
271 pub max_version: Version,
272}
273
274#[derive(Debug)]
275pub struct ListResponsesResponse {
276 pub responses: Vec<ResponseWithCursor>,
277 pub max_cursor: ResponseCursor,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Serialize , schemars::JsonSchema)]
281pub struct JoinSetResponseEventOuter {
282 pub created_at: DateTime<Utc>,
283 pub event: JoinSetResponseEvent,
284}
285
286#[derive(
287 Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
288)]
289pub struct JoinSetResponseEvent {
290 pub join_set_id: JoinSetId,
291 pub event: JoinSetResponse,
292}
293
294#[derive(
295 Clone, Debug, PartialEq, Eq, Serialize, Deserialize, derive_more::Display, schemars::JsonSchema,
296)]
297#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
298#[serde(tag = "type", rename_all = "snake_case")]
299pub enum JoinSetResponse {
300 #[display("delay {}: {delay_id}", if result.is_ok() { "finished" } else { "cancelled"})]
301 DelayFinished {
302 delay_id: DelayId,
303 result: Result<(), ()>,
304 },
305 #[display("{result}: {child_execution_id}")] ChildExecutionFinished {
307 child_execution_id: ExecutionIdDerived,
308 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Version(2)))]
309 finished_version: Version,
310 #[cfg_attr(any(test, feature = "test"), arbitrary(value = crate::SUPPORTED_RETURN_VALUE_OK_EMPTY))]
311 result: SupportedFunctionReturnValue,
312 },
313}
314
315pub const DUMMY_CREATED: ExecutionRequest = ExecutionRequest::Created {
316 ffqn: FunctionFqn::new_static("", ""),
317 params: Params::empty(),
318 parent: None,
319 scheduled_at: DateTime::from_timestamp_nanos(0),
320 component_id: ComponentId::dummy_activity(),
321 deployment_id: DeploymentId::from_parts(0, 0),
322 metadata: ExecutionMetadata::empty(),
323 scheduled_by: None,
324};
325pub const DUMMY_HISTORY_EVENT: ExecutionRequest = ExecutionRequest::HistoryEvent {
326 event: HistoryEvent::JoinSetCreate {
327 join_set_id: JoinSetId {
328 kind: crate::JoinSetKind::OneOff,
329 name: StrVariant::empty(),
330 },
331 },
332};
333
334#[derive(
335 Clone,
336 derive_more::Debug,
337 derive_more::Display,
338 PartialEq,
339 Eq,
340 Serialize,
341 Deserialize,
342 schemars::JsonSchema,
343)]
344#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
345#[serde(rename_all = "snake_case")]
346pub enum ExecutionRequest {
347 #[display("Created({ffqn}, `{scheduled_at}`)")]
348 Created {
349 ffqn: FunctionFqn,
350 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Params::empty()))]
351 #[debug(skip)]
352 params: Params,
353 parent: Option<(ExecutionId, JoinSetId)>,
354 scheduled_at: DateTime<Utc>,
355 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity()))]
356 component_id: ComponentId,
357 deployment_id: DeploymentId,
358 #[cfg_attr(any(test, feature = "test"), arbitrary(default))]
359 metadata: ExecutionMetadata,
360 scheduled_by: Option<ExecutionId>,
361 },
362 Locked(Locked),
363 #[display("Unlocked({_0})")]
372 Unlocked(Unlocked),
373 #[display("ComponentUpgradeFinished({component_digest})")]
375 ComponentUpgradeFinished {
376 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity().component_digest))]
377 component_digest: ComponentDigest,
378 #[cfg_attr(any(test, feature = "test"), arbitrary(value = DeploymentId::from_parts(0, 0)))]
379 deployment_id: DeploymentId,
380 outcome: ComponentUpgradeOutcome,
381 },
382 #[display("TemporarilyFailed(`{backoff_expires_at}`)")]
385 TemporarilyFailed {
386 backoff_expires_at: DateTime<Utc>,
387 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
388 reason: StrVariant,
389 detail: Option<String>,
390 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
391 http_client_traces: Option<Vec<HttpClientTrace>>,
392 },
393 #[display("TemporarilyTimedOut(`{backoff_expires_at}`)")]
396 TemporarilyTimedOut {
397 backoff_expires_at: DateTime<Utc>,
398 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
399 http_client_traces: Option<Vec<HttpClientTrace>>,
400 },
401 #[display("Finished: {retval}")]
403 Finished {
404 #[cfg_attr(any(test, feature = "test"), arbitrary(value = crate::SUPPORTED_RETURN_VALUE_OK_EMPTY))]
405 retval: SupportedFunctionReturnValue,
406 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
407 http_client_traces: Option<Vec<HttpClientTrace>>,
408 },
409
410 #[display("HistoryEvent({event})")]
411 HistoryEvent {
412 event: HistoryEvent,
413 },
414 #[display("Paused")]
415 Paused,
416 #[display("Unpaused")]
417 Unpaused,
418}
419
420#[derive(
422 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
423)]
424#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
425#[serde(tag = "type", rename_all = "snake_case")]
426pub enum ComponentUpgradeReason {
427 #[display("auto")]
428 Auto,
429 #[display("manual(force = {force})")]
430 Manual { force: bool },
431}
432
433#[derive(
434 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
435)]
436#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
437#[display("{reason}, pending at {backoff_expires_at}")]
438pub struct Unlocked {
439 pub backoff_expires_at: DateTime<Utc>,
442 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
443 pub reason: StrVariant,
444}
445
446#[derive(
447 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
448)]
449#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
450#[serde(tag = "type", rename_all = "snake_case")]
451pub enum ComponentUpgradeOutcome {
452 #[display("success({reason})")]
453 Success { reason: ComponentUpgradeReason },
454 #[display("failed: {reason}")]
455 Failed {
456 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
457 reason: StrVariant,
458 },
459}
460
461impl ExecutionRequest {
462 #[must_use]
463 pub fn is_temporary_event(&self) -> bool {
464 matches!(
465 self,
466 Self::TemporarilyFailed { .. } | Self::TemporarilyTimedOut { .. }
467 )
468 }
469
470 #[must_use]
472 pub const fn variant(&self) -> &'static str {
473 match self {
474 ExecutionRequest::Created { .. } => "created",
475 ExecutionRequest::Locked(_) => "locked",
476 ExecutionRequest::Unlocked(_) => "unlocked",
477 ExecutionRequest::ComponentUpgradeFinished { .. } => "component_upgrade_finished",
478 ExecutionRequest::TemporarilyFailed { .. } => "temporarily_failed",
479 ExecutionRequest::TemporarilyTimedOut { .. } => "temporarily_timed_out",
480 ExecutionRequest::Finished { .. } => "finished",
481 ExecutionRequest::HistoryEvent { .. } => "history_event",
482 ExecutionRequest::Paused => "paused",
483 ExecutionRequest::Unpaused => "unpaused",
484 }
485 }
486
487 #[must_use]
488 pub fn join_set_id(&self) -> Option<&JoinSetId> {
489 match self {
490 Self::Created {
491 parent: Some((_parent_id, join_set_id)),
492 ..
493 } => Some(join_set_id),
494 Self::HistoryEvent {
495 event:
496 HistoryEvent::JoinSetCreate { join_set_id, .. }
497 | HistoryEvent::JoinSetRequest { join_set_id, .. }
498 | HistoryEvent::JoinNext { join_set_id, .. },
499 } => Some(join_set_id),
500 _ => None,
501 }
502 }
503}
504
505#[derive(
506 Clone,
507 derive_more::Debug,
508 derive_more::Display,
509 PartialEq,
510 Eq,
511 Serialize,
512 Deserialize,
513 schemars::JsonSchema,
514)]
515#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
516#[display("Locked(`{lock_expires_at}`, {component_id})")]
517pub struct Locked {
518 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity()))]
519 pub component_id: ComponentId,
520 pub executor_id: ExecutorId,
521 pub deployment_id: DeploymentId,
522 pub run_id: RunId,
523 pub lock_expires_at: DateTime<Utc>,
524 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentRetryConfig::ZERO))]
525 pub retry_config: ComponentRetryConfig,
526}
527
528#[derive(
529 Debug,
530 Clone,
531 Copy,
532 PartialEq,
533 Eq,
534 derive_more::Display,
535 Serialize,
536 Deserialize,
537 schemars::JsonSchema,
538)]
539#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
540#[serde(tag = "type", rename_all = "snake_case")]
541pub enum PersistKind {
542 #[display("RandomU64({min}, {max_inclusive})")]
543 RandomU64 {
544 min: u64,
545 max_inclusive: u64,
546 },
547 #[display("RandomString({min_length}, {max_length_exclusive})")]
548 RandomString {
549 min_length: u64,
550 max_length_exclusive: u64,
551 },
552 ExecutionId,
553}
554
555#[must_use]
556pub fn from_u64_to_bytes(value: u64) -> [u8; 8] {
557 value.to_be_bytes()
558}
559
560#[derive(
561 derive_more::Debug,
562 Clone,
563 PartialEq,
564 Eq,
565 derive_more::Display,
566 Serialize,
567 Deserialize,
568 schemars::JsonSchema,
569)]
570#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
571#[serde(tag = "type", rename_all = "snake_case")]
572pub enum HistoryEvent {
574 #[display("Persist")]
576 Persist {
577 #[debug(skip)]
578 value: Vec<u8>, kind: PersistKind,
580 },
581 #[display("JoinSetCreate({join_set_id})")]
582 JoinSetCreate { join_set_id: JoinSetId },
583 #[display("JoinSetRequest({request})")]
584 JoinSetRequest {
586 join_set_id: JoinSetId,
587 request: JoinSetRequest,
588 },
589 #[display("JoinNext({join_set_id})")]
595 JoinNext {
596 join_set_id: JoinSetId,
597 run_expires_at: DateTime<Utc>,
600 requested_ffqn: Option<FunctionFqn>,
603 closing: bool,
605 },
606 #[display("JoinNextTry({join_set_id}, {outcome})")]
608 JoinNextTry {
609 join_set_id: JoinSetId,
610 outcome: JoinNextTryOutcome,
611 },
612 #[display("JoinNextTooMany({join_set_id})")]
614 JoinNextTooMany {
615 join_set_id: JoinSetId,
616 requested_ffqn: Option<FunctionFqn>,
619 },
620 #[display("Schedule({execution_id}, {schedule_at})")]
621 Schedule {
622 execution_id: ExecutionId,
623 schedule_at: HistoryEventScheduleAt, #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
625 result: Result<(), ScheduleRequestError>,
626 },
627 #[display("Stub({target_execution_id})")]
628 Stub {
629 target_execution_id: ExecutionIdDerived,
630 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StubRetVal::Typed(crate::SUPPORTED_RETURN_VALUE_OK_EMPTY).hash()))]
631 retval_hash: StubRetValHash,
632 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
633 result: Result<(), StubError>,
634 },
635}
636
637#[derive(derive_more::Debug, Clone, PartialEq, Eq)]
639#[cfg_attr(any(test, feature = "test"), derive(Serialize, Deserialize))]
640#[cfg_attr(any(test, feature = "test"), serde(rename_all = "snake_case"))]
641pub enum StubRetVal {
642 Typed(SupportedFunctionReturnValue),
643 Untyped(String),
644}
645
646impl StubRetVal {
647 #[must_use]
649 pub fn hash(&self) -> StubRetValHash {
650 use sha2::{Digest as _, Sha256};
651 const STUB_RETVAL_HASH_VERSION: u8 = 1;
652 let mut hasher = Sha256::default();
653
654 match self {
655 StubRetVal::Typed(val) => {
656 hasher.update(b"T|");
657 let json = serde_json::to_string(val)
659 .expect("SupportedFunctionReturnValue is always serializable");
660 hasher.update(json.as_bytes());
661 }
662 StubRetVal::Untyped(s) => {
663 hasher.update(b"U|");
664 hasher.update(s.as_bytes());
665 }
666 }
667
668 let hash_bytes = hasher.finalize();
669 let mut result = [0u8; 33];
670 result[0] = STUB_RETVAL_HASH_VERSION;
671 result[1..].copy_from_slice(&hash_bytes);
672
673 StubRetValHash(result)
674 }
675}
676
677#[derive(
680 Clone,
681 PartialEq,
682 Eq,
683 serde_with::SerializeDisplay,
684 serde_with::DeserializeFromStr,
685 schemars::JsonSchema,
686)]
687#[schemars(with = "String")]
688pub struct StubRetValHash([u8; 33]);
689
690impl Display for StubRetValHash {
691 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692 for b in self.0 {
693 write!(f, "{b:02x}")?;
694 }
695 Ok(())
696 }
697}
698
699impl Debug for StubRetValHash {
700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701 Display::fmt(self, f)
702 }
703}
704
705impl std::str::FromStr for StubRetValHash {
706 type Err = StubRetValHashParseError;
707
708 fn from_str(s: &str) -> Result<Self, Self::Err> {
709 if s.len() != 66 {
710 return Err(StubRetValHashParseError::InvalidLength(s.len()));
712 }
713 let mut bytes = [0u8; 33];
714 for i in 0..33 {
715 let chunk = &s[i * 2..i * 2 + 2];
716 bytes[i] =
717 u8::from_str_radix(chunk, 16).map_err(|_| StubRetValHashParseError::InvalidHex)?;
718 }
719 Ok(StubRetValHash(bytes))
720 }
721}
722
723#[derive(Debug, thiserror::Error)]
724pub enum StubRetValHashParseError {
725 #[error("invalid length: expected 66 hex chars, got {0}")]
726 InvalidLength(usize),
727 #[error("invalid hex character")]
728 InvalidHex,
729}
730
731#[derive(
734 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
735)]
736#[serde(rename_all = "snake_case")]
737pub enum StubError {
738 #[error("execution not found")]
739 ExecutionNotFound,
740 #[error("type check error: {0}")]
741 TypeCheckError(String),
742 #[error("conflict")]
743 Conflict,
744}
745
746#[derive(
748 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
749)]
750#[serde(rename_all = "snake_case")]
751pub enum ScheduleRequestError {
752 #[error("function not found")]
753 FunctionNotFound,
754 #[error("params parsing error: {0}")]
755 TypeCheckError(String),
756}
757
758#[derive(
760 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
761)]
762#[serde(rename_all = "snake_case")]
763pub enum ChildExecutionRequestError {
764 #[error("function not found")]
765 FunctionNotFound,
766 #[error("params parsing error: {0}")]
767 TypeCheckError(String),
768}
769
770#[derive(
771 Debug,
772 Clone,
773 Copy,
774 PartialEq,
775 Eq,
776 derive_more::Display,
777 Serialize,
778 Deserialize,
779 schemars::JsonSchema,
780)]
781#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
782#[serde(rename_all = "snake_case")]
783pub enum JoinNextTryOutcome {
784 #[display("found")]
786 Found,
787 #[display("pending")]
789 Pending,
790 #[display("all_processed")]
792 AllProcessed,
793}
794
795impl From<bool> for JoinNextTryOutcome {
796 fn from(found_response: bool) -> Self {
800 if found_response {
801 JoinNextTryOutcome::Found
802 } else {
803 JoinNextTryOutcome::Pending
804 }
805 }
806}
807
808#[derive(
809 Debug,
810 Clone,
811 Copy,
812 PartialEq,
813 Eq,
814 derive_more::Display,
815 Serialize,
816 Deserialize,
817 schemars::JsonSchema,
818)]
819#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
820#[serde(rename_all = "snake_case")]
821pub enum HistoryEventScheduleAt {
822 Now,
823 #[display("At(`{_0}`)")]
824 At(DateTime<Utc>),
825 #[display("In({_0:?})")]
826 In(Duration),
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
830pub enum ScheduleAtConversionError {
831 #[error("source duration value is out of range")]
832 OutOfRangeError,
833}
834
835impl HistoryEventScheduleAt {
836 pub fn as_date_time(
837 &self,
838 now: DateTime<Utc>,
839 ) -> Result<DateTime<Utc>, ScheduleAtConversionError> {
840 match self {
841 Self::Now => Ok(now),
842 Self::At(date_time) => Ok(*date_time),
843 Self::In(duration) => {
844 let time_delta = TimeDelta::from_std(*duration)
845 .map_err(|_| ScheduleAtConversionError::OutOfRangeError)?;
846 now.checked_add_signed(time_delta)
847 .ok_or(ScheduleAtConversionError::OutOfRangeError)
848 }
849 }
850 }
851}
852
853#[derive(
854 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
855)]
856#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
857#[serde(tag = "type", rename_all = "snake_case")]
858pub enum JoinSetRequest {
859 #[display("DelayRequest({delay_id}, expires_at: `{expires_at}`, schedule_at: `{schedule_at}`)")]
861 DelayRequest {
862 delay_id: DelayId,
863 expires_at: DateTime<Utc>,
864 schedule_at: HistoryEventScheduleAt,
865 #[serde(default)]
866 paused: bool,
867 },
868 #[display("ChildExecutionRequest({child_execution_id}, {target_ffqn}, params: {params})")]
870 ChildExecutionRequest {
871 child_execution_id: ExecutionIdDerived,
872 target_ffqn: FunctionFqn,
873 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Params::empty()))]
874 params: Params,
875 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
876 result: Result<(), ChildExecutionRequestError>,
877 },
878}
879
880#[derive(Debug, Clone, thiserror::Error, derive_more::PartialEq, derive_more::Eq)]
882pub enum DbErrorGeneric {
883 #[error("database error: {reason}")]
884 Uncategorized {
885 reason: StrVariant,
886 #[eq(skip)]
887 #[partial_eq(skip)]
888 context: SpanTrace,
889 #[eq(skip)]
890 #[partial_eq(skip)]
891 #[source]
892 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
893 loc: &'static Location<'static>,
894 },
895 #[error("database was closed")]
896 Close,
897}
898
899#[derive(thiserror::Error, Clone, Debug, derive_more::PartialEq, derive_more::Eq)]
900pub enum DbErrorWriteNonRetriable {
901 #[error("validation failed: {0}")]
902 ValidationFailed(StrVariant),
903 #[error("conflict")]
904 Conflict,
905 #[error("already finished")]
906 AlreadyFinished,
907 #[error("illegal state: {reason}")]
908 IllegalState {
909 reason: StrVariant,
910 #[eq(skip)]
911 #[partial_eq(skip)]
912 context: SpanTrace,
913 #[eq(skip)]
914 #[partial_eq(skip)]
915 #[source]
916 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
917 loc: &'static Location<'static>,
918 },
919 #[error("illegal state: `Unlocked` cannot be appended in state {0}")]
920 UnlockedCannotBeAppended(&'static str),
921 #[error("version conflict: expected: {expected}, got: {requested}")]
922 VersionConflict {
923 expected: Version,
924 requested: Version,
925 },
926}
927
928#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
930pub enum DbErrorWrite {
931 #[error("cannot write - row not found")]
932 NotFound,
933 #[error("non-retriable error: {0}")]
934 NonRetriable(#[from] DbErrorWriteNonRetriable),
935 #[error(transparent)]
936 Generic(#[from] DbErrorGeneric),
937}
938
939#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
941pub enum DbErrorStubResponse {
942 #[error("stub conflict: already finished with a different value")]
943 StubConflict,
944 #[error(transparent)]
945 Write(#[from] DbErrorWrite),
946}
947
948#[derive(Debug, Clone, thiserror::Error, PartialEq)]
950pub enum DbErrorRead {
951 #[error("cannot read - row not found")]
952 NotFound,
953 #[error(transparent)]
954 Generic(#[from] DbErrorGeneric),
955}
956
957#[derive(Debug, thiserror::Error, PartialEq)]
958pub enum DbErrorReadWithTimeout {
959 #[error("timeout")]
960 Timeout(TimeoutOutcome),
961 #[error(transparent)]
962 DbErrorRead(#[from] DbErrorRead),
963}
964
965pub type AppendResponse = Version;
968pub type PendingExecution = (ExecutionId, Version, Params, Option<DateTime<Utc>>);
969
970#[derive(Debug, Clone)]
971pub struct LockedExecution {
972 pub execution_id: ExecutionId,
973 pub next_version: Version,
974 pub metadata: ExecutionMetadata,
975 pub component_digest: ComponentDigest,
976 pub locked_event: Locked,
977 pub ffqn: FunctionFqn,
978 pub params: Params,
979 pub event_history: Vec<(HistoryEvent, Version)>,
980 pub responses: Vec<ResponseWithCursor>,
981 pub parent: Option<(ExecutionId, JoinSetId)>,
982 pub intermittent_event_count: u32,
983}
984
985pub type LockPendingResponse = Vec<LockedExecution>;
986pub type AppendBatchResponse = Version;
987
988#[derive(Debug, Clone, PartialEq, derive_more::Display, Serialize, Deserialize)]
989#[display("{event}")]
990pub struct AppendRequest {
991 pub created_at: DateTime<Utc>,
992 pub event: ExecutionRequest,
993}
994
995#[derive(Debug, Clone, PartialEq)]
996#[cfg_attr(feature = "test", derive(Serialize))]
997pub struct CreateRequest {
998 pub created_at: DateTime<Utc>,
999 pub execution_id: ExecutionId,
1000 pub ffqn: FunctionFqn,
1001 pub params: Params,
1002 pub parent: Option<(ExecutionId, JoinSetId)>,
1003 pub scheduled_at: DateTime<Utc>,
1004 pub component_id: ComponentId,
1005 pub deployment_id: DeploymentId,
1006 pub metadata: ExecutionMetadata,
1007 pub scheduled_by: Option<ExecutionId>,
1008 pub paused: bool,
1009}
1010
1011impl From<CreateRequest> for ExecutionRequest {
1012 fn from(value: CreateRequest) -> Self {
1013 Self::Created {
1014 ffqn: value.ffqn,
1015 params: value.params,
1016 parent: value.parent,
1017 scheduled_at: value.scheduled_at,
1018 component_id: value.component_id,
1019 deployment_id: value.deployment_id,
1020 metadata: value.metadata,
1021 scheduled_by: value.scheduled_by,
1022 }
1023 }
1024}
1025
1026#[async_trait]
1027pub trait DbPool: Send + Sync {
1028 async fn db_exec_conn(&self) -> Result<Box<dyn DbExecutor>, DbErrorGeneric>;
1029
1030 async fn connection(&self) -> Result<Box<dyn DbConnection>, DbErrorGeneric>;
1031
1032 async fn external_api_conn(&self) -> Result<Box<dyn DbExternalApi>, DbErrorGeneric>;
1033
1034 #[cfg(feature = "test")]
1035 async fn connection_test(&self) -> Result<Box<dyn DbConnectionTest>, DbErrorGeneric>;
1036}
1037
1038#[async_trait]
1039pub trait DbPoolCloseable {
1040 async fn close(&self);
1041}
1042
1043#[derive(Clone, Debug, PartialEq)]
1044#[cfg_attr(feature = "test", derive(Serialize))]
1045pub struct AppendEventsToExecution {
1046 pub execution_id: ExecutionId,
1047 pub version: Version,
1048 pub batch: Vec<AppendRequest>,
1049}
1050
1051#[derive(Clone, Debug, PartialEq)]
1052#[cfg_attr(feature = "test", derive(Serialize))]
1053pub struct AppendResponseToExecution {
1054 pub parent_execution_id: ExecutionId,
1055 pub created_at: DateTime<Utc>,
1056 pub join_set_id: JoinSetId,
1057 pub child_execution_id: ExecutionIdDerived,
1058 pub finished_version: Version,
1059 pub result: SupportedFunctionReturnValue,
1060}
1061
1062#[derive(Debug, Clone, PartialEq)]
1066#[cfg_attr(feature = "test", derive(Serialize))]
1067pub enum CapturedDbWrite {
1068 Append {
1069 execution_id: ExecutionId,
1070 version: Version,
1071 req: AppendRequest,
1072 backtraces: Vec<BacktraceInfo>,
1073 },
1074 AppendBatch {
1075 current_time: DateTime<Utc>,
1076 batch: Vec<AppendRequest>,
1077 execution_id: ExecutionId,
1078 version: Version,
1079 backtraces: Vec<BacktraceInfo>,
1080 },
1081 AppendBatchCreateNewExecution {
1082 current_time: DateTime<Utc>,
1083 batch: Vec<AppendRequest>,
1084 execution_id: ExecutionId,
1085 version: Version,
1086 child_req: Vec<CreateRequest>,
1087 backtraces: Vec<BacktraceInfo>,
1088 },
1089 AppendStubResponse {
1090 events: AppendEventsToExecution,
1091 response: AppendResponseToExecution,
1092 current_time: DateTime<Utc>,
1093 backtraces: Vec<BacktraceInfo>,
1094 },
1095 AppendFinished {
1096 execution_id: ExecutionId,
1097 version: Version,
1098 current_time: DateTime<Utc>,
1099 retval: SupportedFunctionReturnValue,
1100 parent: Option<(ExecutionId, JoinSetId)>,
1101 },
1102}
1103impl CapturedDbWrite {
1104 #[must_use]
1105 pub fn is_finished(&self) -> bool {
1106 matches!(self, CapturedDbWrite::AppendFinished { .. })
1107 }
1108}
1109
1110#[async_trait]
1111pub trait DbExecutor: Send + Sync {
1112 #[expect(clippy::too_many_arguments)]
1113 async fn lock_pending_by_ffqns(
1114 &self,
1115 batch_size: u32,
1116 pending_at_or_sooner: DateTime<Utc>,
1117 ffqns: Arc<[FunctionFqn]>,
1118 created_at: DateTime<Utc>,
1119 component_id: ComponentId,
1120 deployment_id: DeploymentId,
1121 executor_id: ExecutorId,
1122 lock_expires_at: DateTime<Utc>,
1123 run_id: RunId,
1124 retry_config: ComponentRetryConfig,
1125 ) -> Result<LockPendingResponse, DbErrorWrite>;
1126
1127 #[expect(clippy::too_many_arguments)]
1128 async fn lock_pending_by_ffqns_auto(
1129 &self,
1130 batch_size: u32,
1131 pending_at_or_sooner: DateTime<Utc>,
1132 ffqns: Arc<[FunctionFqn]>,
1133 created_at: DateTime<Utc>,
1134 component_id: ComponentId,
1135 deployment_id: DeploymentId,
1136 executor_id: ExecutorId,
1137 lock_expires_at: DateTime<Utc>,
1138 run_id: RunId,
1139 retry_config: ComponentRetryConfig,
1140 ) -> Result<LockPendingResponse, DbErrorWrite>;
1141
1142 #[expect(clippy::too_many_arguments)]
1143 async fn lock_pending_by_component_digest(
1144 &self,
1145 batch_size: u32,
1146 pending_at_or_sooner: DateTime<Utc>,
1147 component_id: &ComponentId,
1148 deployment_id: DeploymentId,
1149 created_at: DateTime<Utc>,
1150 executor_id: ExecutorId,
1151 lock_expires_at: DateTime<Utc>,
1152 run_id: RunId,
1153 retry_config: ComponentRetryConfig,
1154 ) -> Result<LockPendingResponse, DbErrorWrite>;
1155
1156 #[cfg(feature = "test")]
1157 #[expect(clippy::too_many_arguments)]
1158 async fn lock_one(
1159 &self,
1160 created_at: DateTime<Utc>,
1161 component_id: ComponentId,
1162 deployment_id: DeploymentId,
1163 execution_id: &ExecutionId,
1164 run_id: RunId,
1165 version: Version,
1166 executor_id: ExecutorId,
1167 lock_expires_at: DateTime<Utc>,
1168 retry_config: ComponentRetryConfig,
1169 ) -> Result<LockedExecution, DbErrorWrite>;
1170
1171 async fn append(
1174 &self,
1175 execution_id: ExecutionId,
1176 version: Version,
1177 req: AppendRequest,
1178 ) -> Result<AppendResponse, DbErrorWrite>;
1179
1180 async fn append_batch_respond_to_parent(
1183 &self,
1184 events: AppendEventsToExecution,
1185 response: AppendResponseToExecution,
1186 current_time: DateTime<Utc>, ) -> Result<AppendBatchResponse, DbErrorWrite>;
1188
1189 async fn wait_for_pending_by_ffqn(
1195 &self,
1196 pending_at_or_sooner: DateTime<Utc>,
1197 ffqns: Arc<[FunctionFqn]>,
1198 current_digest: Option<ComponentDigest>,
1199 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
1200 );
1201
1202 async fn wait_for_pending_by_component_digest(
1207 &self,
1208 pending_at_or_sooner: DateTime<Utc>,
1209 component_digest: &ComponentDigest,
1210 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
1211 );
1212
1213 async fn cancel_activity_with_retries(
1214 &self,
1215 execution_id: &ExecutionId,
1216 cancelled_at: DateTime<Utc>,
1217 ) -> Result<CancelOutcome, DbErrorWrite> {
1218 let mut retries = 5;
1219 loop {
1220 let res = self.cancel_activity(execution_id, cancelled_at).await;
1221 if res.is_ok() || retries == 0 {
1222 return res;
1223 }
1224 retries -= 1;
1225 }
1226 }
1227
1228 async fn get_last_execution_event(
1230 &self,
1231 execution_id: &ExecutionId,
1232 ) -> Result<ExecutionEvent, DbErrorRead>;
1233
1234 async fn cancel_activity(
1235 &self,
1236 execution_id: &ExecutionId,
1237 cancelled_at: DateTime<Utc>,
1238 ) -> Result<CancelOutcome, DbErrorWrite> {
1239 debug!("Determining cancellation state of {execution_id}");
1240
1241 let last_event = self
1242 .get_last_execution_event(execution_id)
1243 .await
1244 .map_err(DbErrorWrite::from)?;
1245 if let ExecutionRequest::Finished {
1246 retval:
1247 SupportedFunctionReturnValue::ExecutionFailure(FinishedExecutionFailure {
1248 kind: ExecutionFailureKind::Cancelled,
1249 ..
1250 }),
1251 ..
1252 } = last_event.event
1253 {
1254 return Ok(CancelOutcome::Cancelled);
1255 } else if matches!(last_event.event, ExecutionRequest::Finished { .. }) {
1256 debug!("Not cancelling, {execution_id} is already finished");
1257 return Ok(CancelOutcome::AlreadyFinished);
1258 }
1259 let finished_version = last_event.version.increment();
1260 let child_result =
1261 SupportedFunctionReturnValue::ExecutionFailure(FinishedExecutionFailure {
1262 reason: None,
1263 kind: ExecutionFailureKind::Cancelled,
1264 detail: None,
1265 });
1266 let cancel_request = AppendRequest {
1267 created_at: cancelled_at,
1268 event: ExecutionRequest::Finished {
1269 retval: child_result.clone(),
1270 http_client_traces: None,
1271 },
1272 };
1273 debug!("Cancelling activity {execution_id} at {finished_version}");
1274 if let ExecutionId::Derived(execution_id) = execution_id {
1275 let (parent_execution_id, join_set_id) = execution_id.split_to_parts();
1276 let child_execution_id = ExecutionId::Derived(execution_id.clone());
1277 self.append_batch_respond_to_parent(
1278 AppendEventsToExecution {
1279 execution_id: child_execution_id,
1280 version: finished_version.clone(),
1281 batch: vec![cancel_request],
1282 },
1283 AppendResponseToExecution {
1284 parent_execution_id,
1285 created_at: cancelled_at,
1286 join_set_id: join_set_id.clone(),
1287 child_execution_id: execution_id.clone(),
1288 finished_version,
1289 result: child_result,
1290 },
1291 cancelled_at,
1292 )
1293 .await?;
1294 } else {
1295 self.append(execution_id.clone(), finished_version, cancel_request)
1296 .await?;
1297 }
1298 debug!("Cancelled {execution_id}");
1299 Ok(CancelOutcome::Cancelled)
1300 }
1301}
1302
1303pub enum AppendDelayResponseOutcome {
1304 Success,
1305 AlreadyFinished,
1306 AlreadyCancelled,
1307}
1308
1309#[derive(Debug, Clone, Default)]
1310pub struct ListExecutionsFilter {
1311 pub function_name_filter: Option<FunctionNameFilter>,
1312 pub show_derived: bool,
1313 pub hide_finished: bool,
1314 pub execution_id_prefix: Option<String>,
1315 pub component_digest: Option<ComponentDigest>,
1316 pub deployment_id: Option<DeploymentId>,
1317}
1318
1319#[derive(Debug, Clone, PartialEq, Eq)]
1320pub enum FunctionNameFilter {
1321 PackageName(String),
1322 InterfaceName(String),
1323 FunctionName(String),
1324}
1325
1326impl FunctionNameFilter {
1327 #[must_use]
1328 pub fn like_pattern(&self) -> String {
1329 match self {
1330 Self::FunctionName(function_name) | Self::InterfaceName(function_name) => {
1331 format!("{function_name}%")
1332 }
1333 Self::PackageName(package_name) => {
1334 if let Some((pkg_fqn_without_version, version)) = package_name.rsplit_once('@')
1335 && !version.is_empty()
1336 && pkg_fqn_without_version.contains(':')
1337 {
1338 format!("{pkg_fqn_without_version}/%@{version}.%")
1339 } else {
1340 format!("{package_name}%")
1341 }
1342 }
1343 }
1344 }
1345}
1346
1347#[async_trait]
1348pub trait DbExternalApi: DbConnection {
1349 async fn get_backtrace(
1351 &self,
1352 execution_id: &ExecutionId,
1353 filter: BacktraceFilter,
1354 ) -> Result<BacktraceInfo, DbErrorRead>;
1355
1356 async fn upsert_source_file(
1360 &self,
1361 component_digest: &ComponentDigest,
1362 frame_key: &str,
1363 is_suffix: bool,
1364 content: &str,
1365 ) -> Result<(), DbErrorWrite>;
1366
1367 async fn get_source_file(
1371 &self,
1372 component_digest: &ComponentDigest,
1373 file: &str,
1374 ) -> Result<Option<String>, DbErrorRead>;
1375
1376 async fn upsert_component_metadata(
1378 &self,
1379 records: Vec<ComponentMetadataRecord>,
1380 ) -> Result<(), DbErrorWrite>;
1381
1382 async fn insert_deployment_components(
1384 &self,
1385 deployment_id: DeploymentId,
1386 records: Vec<DeploymentComponentRecord>,
1387 ) -> Result<(), DbErrorWrite>;
1388
1389 async fn list_deployment_components(
1391 &self,
1392 deployment_id: DeploymentId,
1393 ) -> Result<Vec<DeploymentComponentDetail>, DbErrorRead>;
1394
1395 async fn get_deployment_component_wit(
1397 &self,
1398 deployment_id: DeploymentId,
1399 component_digest: &ComponentDigest,
1400 ) -> Result<Option<String>, DbErrorRead>;
1401
1402 async fn list_executions(
1404 &self,
1405 filter: ListExecutionsFilter,
1406 pagination: ExecutionListPagination,
1407 ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric>;
1408
1409 async fn list_execution_events(
1414 &self,
1415 execution_id: &ExecutionId,
1416 pagination: Pagination<VersionType>,
1417 include_backtrace_id: bool,
1418 ) -> Result<ListExecutionEventsResponse, DbErrorRead>;
1419
1420 async fn list_responses(
1429 &self,
1430 execution_id: &ExecutionId,
1431 pagination: Pagination<u32>,
1432 ) -> Result<ListResponsesResponse, DbErrorRead>;
1433
1434 async fn list_execution_events_responses(
1435 &self,
1436 execution_id: &ExecutionId,
1437 req_since: &Version,
1438 req_max_length: VersionType,
1439 req_include_backtrace_id: bool,
1440 resp_pagination: Pagination<VersionType>,
1441 ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead>;
1442
1443 async fn upgrade_execution_component(
1444 &self,
1445 execution_id: &ExecutionId,
1446 old: &ComponentDigest,
1447 new: &ComponentDigest,
1448 reason: ComponentUpgradeReason,
1449 ) -> Result<(), DbErrorWrite>;
1450
1451 async fn list_logs(
1452 &self,
1453 execution_id: &ExecutionId,
1454 show_derived: bool,
1455 filter: LogFilter,
1456 pagination: Pagination<DateTime<Utc>>,
1457 ) -> Result<ListLogsResponse, DbErrorRead>;
1458
1459 async fn list_deployment_states(
1460 &self,
1461 current_time: DateTime<Utc>,
1462 pagination: Pagination<Option<DeploymentId>>,
1463 include_config_json: bool,
1464 ) -> Result<Vec<DeploymentState>, DbErrorRead>;
1465
1466 async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite>;
1469
1470 async fn activate_deployment(
1471 &self,
1472 deployment_id: DeploymentId,
1473 now: DateTime<Utc>,
1474 ) -> Result<(), DbErrorWrite>;
1475
1476 async fn enqueue_deployment(&self, deployment_id: DeploymentId) -> Result<(), DbErrorWrite>;
1480
1481 async fn get_deployment(
1483 &self,
1484 deployment_id: DeploymentId,
1485 ) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1486
1487 #[cfg(feature = "test")]
1490 async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1491
1492 async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1495
1496 async fn list_deployments(
1497 &self,
1498 pagination: Pagination<Option<DeploymentId>>,
1499 ) -> Result<Vec<DeploymentRecord>, DbErrorRead>;
1500
1501 async fn pause_execution(
1505 &self,
1506 execution_id: &ExecutionId,
1507 paused_at: DateTime<Utc>,
1508 ) -> Result<AppendResponse, DbErrorWrite>;
1509
1510 async fn unpause_execution(
1512 &self,
1513 execution_id: &ExecutionId,
1514 unpaused_at: DateTime<Utc>,
1515 ) -> Result<AppendResponse, DbErrorWrite>;
1516
1517 async fn pause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite>;
1521
1522 async fn unpause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite>;
1526}
1527pub const LIST_DEPLOYMENT_STATES_DEFAULT_LENGTH: u16 = 20;
1528pub const LIST_DEPLOYMENT_STATES_DEFAULT_PAGINATION: Pagination<Option<DeploymentId>> =
1529 Pagination::OlderThan {
1530 length: LIST_DEPLOYMENT_STATES_DEFAULT_LENGTH,
1531 cursor: None,
1532 including_cursor: false,
1533 };
1534
1535pub struct DeploymentState {
1536 pub deployment_id: DeploymentId,
1537 pub locked: u32,
1538 pub pending: u32,
1540 pub scheduled: u32,
1542 pub blocked: u32,
1543 pub finished: u32,
1544 pub config_json: Option<String>,
1546 pub created_at: DateTime<Utc>,
1547 pub last_active_at: Option<DateTime<Utc>>,
1549 pub status: DeploymentStatus,
1550}
1551
1552#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1553pub enum DeploymentStatus {
1554 Inactive,
1555 Enqueued,
1557 Active,
1558}
1559
1560impl DeploymentStatus {
1561 #[must_use]
1562 pub fn as_str(&self) -> &'static str {
1563 match self {
1564 DeploymentStatus::Inactive => "inactive",
1565 DeploymentStatus::Enqueued => "enqueued",
1566 DeploymentStatus::Active => "active",
1567 }
1568 }
1569}
1570
1571impl std::str::FromStr for DeploymentStatus {
1572 type Err = StrVariant;
1573 fn from_str(s: &str) -> Result<Self, Self::Err> {
1574 match s {
1575 "inactive" => Ok(DeploymentStatus::Inactive),
1576 "enqueued" => Ok(DeploymentStatus::Enqueued),
1577 "active" => Ok(DeploymentStatus::Active),
1578 _ => Err(StrVariant::from(format!("unknown deployment status: {s}"))),
1579 }
1580 }
1581}
1582
1583#[derive(Debug, Clone)]
1584pub struct DeploymentRecord {
1585 pub deployment_id: DeploymentId,
1586 pub created_at: DateTime<Utc>,
1587 pub last_active_at: Option<DateTime<Utc>>,
1589 pub status: DeploymentStatus,
1590 pub config_json: String, pub obelisk_version: String,
1592 pub created_by: Option<String>,
1593}
1594
1595#[derive(Debug, Clone)]
1596pub struct ComponentMetadataRecord {
1597 pub component_digest: ComponentDigest,
1598 pub imports: Vec<PersistedFunctionMetadata>,
1599 pub exports: Vec<PersistedFunctionMetadata>,
1600 pub wit: String,
1601 pub wit_origin: String,
1602}
1603
1604#[derive(Debug, Clone)]
1606pub struct DeploymentComponentRecord {
1607 pub deployment_id: DeploymentId,
1608 pub component_name: StrVariant,
1609 pub component_digest: ComponentDigest,
1610 pub component_type: ComponentType,
1611}
1612
1613#[derive(Debug, Clone)]
1614pub struct DeploymentComponentDetail {
1615 pub component_id: ComponentId,
1616 pub imports: Vec<PersistedFunctionMetadata>,
1617 pub exports: Vec<PersistedFunctionMetadata>,
1618 pub wit: String,
1619}
1620
1621#[derive(
1622 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, schemars::JsonSchema,
1623)]
1624pub struct PersistedFunctionMetadata {
1625 pub ffqn: FunctionFqn,
1626 pub parameter_types: Vec<PersistedParameterType>,
1627 pub return_type: String,
1628 pub extension: Option<FunctionExtension>,
1629 pub submittable: bool,
1630}
1631
1632#[derive(
1633 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, schemars::JsonSchema,
1634)]
1635pub struct PersistedParameterType {
1636 pub name: String,
1637 pub wit_type: String,
1638}
1639
1640impl From<FunctionMetadata> for PersistedFunctionMetadata {
1641 fn from(value: FunctionMetadata) -> Self {
1642 PersistedFunctionMetadata {
1643 ffqn: value.ffqn,
1644 parameter_types: value
1645 .parameter_types
1646 .0
1647 .into_iter()
1648 .map(|param| PersistedParameterType {
1649 name: param.name.to_string(),
1650 wit_type: param.wit_type.to_string(),
1651 })
1652 .collect(),
1653 return_type: value.return_type.wit_type().to_string(),
1654 extension: value.extension,
1655 submittable: value.submittable,
1656 }
1657 }
1658}
1659
1660#[derive(Debug)]
1661pub struct ListLogsResponse {
1662 pub items: Vec<LogEntryRow>,
1663 pub next_page: Pagination<DateTime<Utc>>, pub prev_page: Option<Pagination<DateTime<Utc>>>, }
1666
1667#[derive(Debug)]
1668pub struct LogFilter {
1669 show_logs: bool,
1670 show_streams: bool,
1671 levels: Vec<LogLevel>, stream_types: Vec<LogStreamType>, }
1674impl LogFilter {
1675 #[must_use]
1677 pub fn show_logs(levels: Vec<LogLevel>) -> LogFilter {
1678 LogFilter {
1679 show_logs: true,
1680 show_streams: false,
1681 levels,
1682 stream_types: Vec::new(),
1683 }
1684 }
1685 #[must_use]
1687 pub fn show_streams(stream_types: Vec<LogStreamType>) -> LogFilter {
1688 LogFilter {
1689 show_logs: false,
1690 show_streams: true,
1691 levels: Vec::new(),
1692 stream_types,
1693 }
1694 }
1695 #[must_use]
1697 pub fn show_combined(levels: Vec<LogLevel>, stream_types: Vec<LogStreamType>) -> LogFilter {
1698 LogFilter {
1699 show_logs: true,
1700 show_streams: true,
1701 levels,
1702 stream_types,
1703 }
1704 }
1705 #[must_use]
1707 pub fn should_show_logs(&self) -> bool {
1708 self.show_logs
1709 }
1710 #[must_use]
1711 pub fn should_show_streams(&self) -> bool {
1712 self.show_streams
1713 }
1714 #[must_use]
1715 pub fn levels(&self) -> &Vec<LogLevel> {
1716 &self.levels
1717 }
1718 #[must_use]
1719 pub fn stream_types(&self) -> &Vec<LogStreamType> {
1720 &self.stream_types
1721 }
1722}
1723
1724#[derive(Debug, Clone)]
1725pub struct ExecutionWithStateRequestsResponses {
1726 pub execution_with_state: ExecutionWithState,
1727 pub events: Vec<ExecutionEvent>,
1728 pub responses: Vec<ResponseWithCursor>,
1729 pub max_version: Version,
1730 pub max_cursor: ResponseCursor,
1731}
1732
1733#[async_trait]
1734pub trait DbConnection: DbExecutor {
1735 async fn get(&self, execution_id: &ExecutionId) -> Result<ExecutionLog, DbErrorRead>;
1737
1738 async fn append_delay_response(
1739 &self,
1740 created_at: DateTime<Utc>,
1741 execution_id: ExecutionId,
1742 join_set_id: JoinSetId,
1743 delay_id: DelayId,
1744 outcome: Result<(), ()>, ) -> Result<AppendDelayResponseOutcome, DbErrorWrite>;
1746
1747 async fn append_batch(
1750 &self,
1751 current_time: DateTime<Utc>, batch: Vec<AppendRequest>,
1753 execution_id: ExecutionId,
1754 version: Version,
1755 ) -> Result<AppendBatchResponse, DbErrorWrite>;
1756
1757 async fn append_batch_create_new_execution(
1760 &self,
1761 current_time: DateTime<Utc>, batch: Vec<AppendRequest>, execution_id: ExecutionId,
1764 version: Version,
1765 child_req: Vec<CreateRequest>,
1766 backtraces: Vec<BacktraceInfo>,
1767 ) -> Result<AppendBatchResponse, DbErrorWrite>;
1768
1769 async fn get_execution_event(
1771 &self,
1772 execution_id: &ExecutionId,
1773 version: &Version,
1774 ) -> Result<ExecutionEvent, DbErrorRead>;
1775
1776 async fn upsert_stub_response(
1780 &self,
1781 execution_id: ExecutionIdDerived,
1782 version: Version,
1783 req: AppendRequest,
1784 response: AppendResponseToExecution,
1785 current_time: DateTime<Utc>,
1786 ) -> Result<(), DbErrorStubResponse>;
1787
1788 #[instrument(skip(self))]
1789 async fn get_create_request(
1790 &self,
1791 execution_id: &ExecutionId,
1792 ) -> Result<CreateRequest, DbErrorRead> {
1793 let execution_event = self
1794 .get_execution_event(execution_id, &Version::new(0))
1795 .await?;
1796 if let ExecutionRequest::Created {
1797 ffqn,
1798 params,
1799 parent,
1800 scheduled_at,
1801 component_id,
1802 deployment_id,
1803 metadata,
1804 scheduled_by,
1805 } = execution_event.event
1806 {
1807 Ok(CreateRequest {
1808 created_at: execution_event.created_at,
1809 execution_id: execution_id.clone(),
1810 ffqn,
1811 params,
1812 parent,
1813 scheduled_at,
1814 component_id,
1815 deployment_id,
1816 metadata,
1817 scheduled_by,
1818 paused: false,
1819 })
1820 } else {
1821 Err(DbErrorRead::Generic(DbErrorGeneric::Uncategorized {
1822 reason: "execution log must start with creation".into(),
1823 context: SpanTrace::capture(),
1824 source: None,
1825 loc: Location::caller(),
1826 }))
1827 }
1828 }
1829
1830 async fn get_pending_state(
1831 &self,
1832 execution_id: &ExecutionId,
1833 ) -> Result<ExecutionWithState, DbErrorRead>;
1834
1835 async fn get_expired_timers(
1837 &self,
1838 at: DateTime<Utc>,
1839 ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric>;
1840
1841 async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite>;
1843
1844 async fn subscribe_to_next_responses(
1851 &self,
1852 execution_id: &ExecutionId,
1853 last_response: ResponseCursor,
1854 timeout_fut: Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>,
1855 ) -> Result<Vec<ResponseWithCursor>, DbErrorReadWithTimeout>;
1856
1857 async fn wait_for_finished_result(
1864 &self,
1865 execution_id: &ExecutionId,
1866 timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
1867 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout>;
1868
1869 async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite>;
1870
1871 async fn append_backtrace_batch(&self, batch: Vec<BacktraceInfo>) -> Result<(), DbErrorWrite>;
1872
1873 async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite>;
1874
1875 async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite>;
1876
1877 #[cfg(feature = "test")]
1879 async fn get_finished_result(
1880 &self,
1881 execution_id: &ExecutionId,
1882 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
1883 self.wait_for_finished_result(
1884 execution_id,
1885 Some(Box::pin(std::future::ready(TimeoutOutcome::Timeout))),
1886 )
1887 .await
1888 }
1889}
1890
1891#[derive(Clone, Debug)]
1892pub struct LogInfoAppendRow {
1893 pub execution_id: ExecutionId,
1894 pub run_id: RunId,
1895 pub log_entry: LogEntry,
1896}
1897
1898#[derive(Debug, Clone)]
1899pub struct LogEntryRow {
1900 pub cursor: DateTime<Utc>,
1901 pub run_id: RunId,
1902 pub log_entry: LogEntry,
1903 pub execution_id: ExecutionId,
1904}
1905
1906#[derive(Debug, Clone)]
1907pub enum LogEntry {
1908 Log {
1909 created_at: DateTime<Utc>,
1910 level: LogLevel,
1911 message: String,
1912 },
1913 Stream {
1914 created_at: DateTime<Utc>,
1915 payload: Vec<u8>,
1916 stream_type: LogStreamType,
1917 },
1918}
1919impl LogEntry {
1920 #[must_use]
1921 pub fn created_at(&self) -> DateTime<Utc> {
1922 match self {
1923 LogEntry::Log { created_at, .. } | LogEntry::Stream { created_at, .. } => *created_at,
1924 }
1925 }
1926}
1927
1928#[derive(
1929 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, derive_more::TryFrom, strum::EnumIter,
1930)]
1931#[try_from(repr)]
1932#[repr(u8)]
1933pub enum LogLevel {
1934 Trace = 1,
1935 Debug,
1936 Info,
1937 Warn,
1938 Error,
1939}
1940#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::TryFrom, strum::EnumIter)]
1941#[try_from(repr)]
1942#[repr(u8)]
1943pub enum LogStreamType {
1944 StdOut = 1,
1945 StdErr,
1946}
1947
1948#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1949pub enum TimeoutOutcome {
1950 Timeout,
1951 Cancel,
1952}
1953
1954#[cfg(feature = "test")]
1955#[async_trait]
1956pub trait DbConnectionTest: DbConnection {
1957 async fn append_response(
1958 &self,
1959 created_at: DateTime<Utc>,
1960 execution_id: ExecutionId,
1961 response_event: JoinSetResponseEvent,
1962 ) -> Result<(), DbErrorWrite>;
1963}
1964
1965#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1966pub enum CancelOutcome {
1967 Cancelled,
1968 AlreadyFinished,
1969}
1970
1971#[instrument(skip(db_connection))]
1972pub async fn stub_execution(
1973 db_connection: &dyn DbConnection,
1974 execution_id: ExecutionIdDerived,
1975 parent_execution_id: ExecutionId,
1976 join_set_id: JoinSetId,
1977 created_at: DateTime<Utc>,
1978 return_value: SupportedFunctionReturnValue,
1979) -> Result<(), DbErrorWrite> {
1980 let stub_finished_version = Version::new(1); let finished_req = AppendRequest {
1982 created_at,
1983 event: ExecutionRequest::Finished {
1984 retval: return_value.clone(),
1985 http_client_traces: None,
1986 },
1987 };
1988 db_connection
1989 .upsert_stub_response(
1990 execution_id.clone(),
1991 stub_finished_version.clone(),
1992 finished_req,
1993 AppendResponseToExecution {
1994 parent_execution_id,
1995 created_at,
1996 join_set_id,
1997 child_execution_id: execution_id,
1998 finished_version: stub_finished_version,
1999 result: return_value,
2000 },
2001 created_at,
2002 )
2003 .await
2004 .map_err(|err| match err {
2005 DbErrorStubResponse::StubConflict => {
2006 DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)
2007 }
2008 DbErrorStubResponse::Write(db_err) => db_err,
2009 })
2010}
2011
2012pub async fn cancel_delay(
2013 db_connection: &dyn DbConnection,
2014 delay_id: DelayId,
2015 cancelled_at: DateTime<Utc>,
2016) -> Result<CancelOutcome, DbErrorWrite> {
2017 let (parent_execution_id, join_set_id) = delay_id.split_to_parts();
2018 db_connection
2019 .append_delay_response(
2020 cancelled_at,
2021 parent_execution_id,
2022 join_set_id,
2023 delay_id,
2024 Err(()), )
2026 .await
2027 .map(|ok| match ok {
2028 AppendDelayResponseOutcome::Success | AppendDelayResponseOutcome::AlreadyCancelled => {
2029 CancelOutcome::Cancelled
2030 }
2031 AppendDelayResponseOutcome::AlreadyFinished => CancelOutcome::AlreadyFinished,
2032 })
2033}
2034
2035#[derive(Clone, Debug)]
2036pub enum BacktraceFilter {
2037 First,
2038 Last,
2039 Specific(Version),
2040}
2041
2042#[derive(Clone, Debug, PartialEq, Eq)]
2043#[cfg_attr(feature = "test", derive(Serialize))]
2044pub struct BacktraceInfo {
2045 pub execution_id: ExecutionId,
2046 pub component_id: ComponentId,
2047 pub version_min_including: Version,
2048 pub version_max_excluding: Version,
2049 pub wasm_backtrace: WasmBacktrace,
2050}
2051
2052#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2053pub struct WasmBacktrace {
2054 pub frames: Vec<FrameInfo>,
2055}
2056
2057#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2058pub struct FrameInfo {
2059 pub module: String,
2060 pub func_name: String,
2061 pub symbols: Vec<FrameSymbol>,
2062}
2063
2064#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2065pub struct FrameSymbol {
2066 pub func_name: Option<String>,
2067 pub file: Option<String>,
2068 pub line: Option<u32>,
2069 pub col: Option<u32>,
2070}
2071
2072mod wasm_backtrace {
2073 use super::{FrameInfo, FrameSymbol, WasmBacktrace};
2074
2075 impl WasmBacktrace {
2076 pub fn maybe_from(backtrace: &wasmtime::WasmBacktrace) -> Option<Self> {
2077 if backtrace.frames().is_empty() {
2078 None
2079 } else {
2080 Some(Self {
2081 frames: backtrace.frames().iter().map(FrameInfo::from).collect(),
2082 })
2083 }
2084 }
2085 }
2086
2087 impl From<&wasmtime::FrameInfo> for FrameInfo {
2088 fn from(frame: &wasmtime::FrameInfo) -> Self {
2089 let module_name = frame.module().name().unwrap_or("<unknown>").to_string();
2090 let mut func_name = String::new();
2091 wasmtime_environ::demangle_function_name_or_index(
2092 &mut func_name,
2093 frame.func_name(),
2094 frame.func_index() as usize,
2095 )
2096 .expect("writing to string must succeed");
2097 Self {
2098 module: module_name,
2099 func_name,
2100 symbols: frame
2101 .symbols()
2102 .iter()
2103 .map(std::convert::Into::into)
2104 .collect(),
2105 }
2106 }
2107 }
2108
2109 impl From<&wasmtime::FrameSymbol> for FrameSymbol {
2110 fn from(symbol: &wasmtime::FrameSymbol) -> Self {
2111 let func_name = symbol.name().map(|name| {
2112 let mut writer = String::new();
2113 wasmtime_environ::demangle_function_name(&mut writer, name)
2114 .expect("writing to string must succeed");
2115 writer
2116 });
2117
2118 Self {
2119 func_name,
2120 file: symbol.file().map(ToString::to_string),
2121 line: symbol.line(),
2122 col: symbol.column(),
2123 }
2124 }
2125 }
2126}
2127#[derive(Debug, Clone, derive_more::Display)]
2128#[display("{execution_id} {pending_state} {component_digest}")]
2129pub struct ExecutionWithState {
2130 pub execution_id: ExecutionId,
2131 pub ffqn: FunctionFqn,
2132 pub pending_state: PendingState,
2133 pub created_at: DateTime<Utc>,
2134 pub first_scheduled_at: DateTime<Utc>,
2135 pub component_digest: ComponentDigest,
2136 pub component_type: ComponentType,
2137 pub deployment_id: DeploymentId,
2138}
2139
2140#[derive(Debug, Clone)]
2141pub enum ExecutionListPagination {
2142 CreatedBy(Pagination<Option<DateTime<Utc>>>),
2143 ExecutionId(Pagination<Option<ExecutionId>>),
2144}
2145impl Default for ExecutionListPagination {
2146 fn default() -> ExecutionListPagination {
2147 ExecutionListPagination::CreatedBy(Pagination::OlderThan {
2148 length: 20,
2149 cursor: None,
2150 including_cursor: false, })
2152 }
2153}
2154impl ExecutionListPagination {
2155 #[must_use]
2156 pub fn length(&self) -> u16 {
2157 match self {
2158 ExecutionListPagination::CreatedBy(pagination) => pagination.length(),
2159 ExecutionListPagination::ExecutionId(pagination) => pagination.length(),
2160 }
2161 }
2162}
2163
2164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2165pub enum Pagination<T> {
2166 NewerThan {
2167 length: u16,
2168 cursor: T,
2169 including_cursor: bool,
2170 },
2171 OlderThan {
2172 length: u16,
2173 cursor: T,
2174 including_cursor: bool,
2175 },
2176}
2177impl<T: Clone> Pagination<T> {
2178 pub fn length(&self) -> u16 {
2179 match self {
2180 Pagination::NewerThan { length, .. } | Pagination::OlderThan { length, .. } => *length,
2181 }
2182 }
2183
2184 pub fn rel(&self) -> &'static str {
2185 match self {
2186 Pagination::NewerThan {
2187 including_cursor: false,
2188 ..
2189 } => ">",
2190 Pagination::NewerThan {
2191 including_cursor: true,
2192 ..
2193 } => ">=",
2194 Pagination::OlderThan {
2195 including_cursor: false,
2196 ..
2197 } => "<",
2198 Pagination::OlderThan {
2199 including_cursor: true,
2200 ..
2201 } => "<=",
2202 }
2203 }
2204
2205 pub fn is_desc(&self) -> bool {
2206 matches!(self, Pagination::OlderThan { .. })
2207 }
2208
2209 pub fn asc_or_desc(&self) -> &'static str {
2210 if self.is_asc() { "asc" } else { "desc" }
2211 }
2212
2213 pub fn is_asc(&self) -> bool {
2214 !self.is_desc()
2215 }
2216
2217 pub fn cursor(&self) -> &T {
2218 match self {
2219 Pagination::NewerThan { cursor, .. } | Pagination::OlderThan { cursor, .. } => cursor,
2220 }
2221 }
2222
2223 #[must_use]
2224 pub fn invert(&self) -> Self {
2225 match self {
2226 Pagination::NewerThan {
2227 length,
2228 cursor,
2229 including_cursor,
2230 } => Pagination::OlderThan {
2231 length: *length,
2232 cursor: cursor.clone(),
2233 including_cursor: !including_cursor,
2234 },
2235 Pagination::OlderThan {
2236 length,
2237 cursor,
2238 including_cursor,
2239 } => Pagination::NewerThan {
2240 length: *length,
2241 cursor: cursor.clone(),
2242 including_cursor: !including_cursor,
2243 },
2244 }
2245 }
2246}
2247
2248#[cfg(feature = "test")]
2249pub async fn wait_for_pending_state_fn<T: Debug>(
2250 db_connection: &dyn DbConnectionTest,
2251 execution_id: &ExecutionId,
2252 predicate: impl Fn(ExecutionLog) -> Option<T> + Send,
2253 timeout: Option<Duration>,
2254) -> Result<T, DbErrorReadWithTimeout> {
2255 tracing::trace!(%execution_id, "Waiting for predicate");
2256 let fut = async move {
2257 loop {
2258 let execution_log = db_connection.get(execution_id).await?;
2259 if let Some(t) = predicate(execution_log) {
2260 tracing::debug!(%execution_id, "Found: {t:?}");
2261 return Ok(t);
2262 }
2263 tokio::time::sleep(Duration::from_millis(10)).await;
2264 }
2265 };
2266
2267 if let Some(timeout) = timeout {
2268 tokio::select! { res = fut => res,
2270 () = tokio::time::sleep(timeout) => Err(DbErrorReadWithTimeout::Timeout(TimeoutOutcome::Timeout))
2271 }
2272 } else {
2273 fut.await
2274 }
2275}
2276
2277#[derive(Debug, Clone, PartialEq, Eq)]
2278pub enum ExpiredTimer {
2279 Lock(ExpiredLock),
2280 Delay(ExpiredDelay),
2281}
2282
2283#[derive(Debug, Clone, PartialEq, Eq)]
2284pub struct ExpiredLock {
2285 pub execution_id: ExecutionId,
2286 pub locked_at_version: Version,
2288 pub next_version: Version,
2289 pub intermittent_event_count: u32,
2291 pub max_retries: Option<u32>,
2292 pub retry_exp_backoff: Duration,
2293 pub locked_by: LockedBy,
2294}
2295
2296#[derive(Debug, Clone, PartialEq, Eq)]
2297pub struct ExpiredDelay {
2298 pub execution_id: ExecutionId,
2299 pub join_set_id: JoinSetId,
2300 pub delay_id: DelayId,
2301}
2302
2303#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2304#[serde(tag = "status", rename_all = "snake_case")]
2305pub enum PendingState {
2306 Locked(PendingStateLocked),
2308
2309 #[display("PendingAt(`{_0}`)")]
2310 PendingAt(PendingStatePendingAt),
2311
2312 #[display("BlockedByJoinSet({_0})")]
2314 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2315
2316 #[display("Paused({_0})")]
2317 Paused(PendingStatePaused),
2318
2319 #[display("Finished: {_0}")]
2320 Finished(PendingStateFinished),
2321}
2322
2323pub enum PendingStateMergedPause {
2324 Locked {
2325 state: PendingStateLocked,
2326 paused: bool,
2327 },
2328 PendingAt {
2329 state: PendingStatePendingAt,
2330 paused: bool,
2331 },
2332 BlockedByJoinSet {
2333 state: PendingStateBlockedByJoinSet,
2334 paused: bool,
2335 },
2336 Finished(PendingStateFinished),
2337}
2338impl From<PendingState> for PendingStateMergedPause {
2339 fn from(state: PendingState) -> Self {
2340 match state {
2341 PendingState::Locked(s) => PendingStateMergedPause::Locked {
2342 state: s,
2343 paused: false,
2344 },
2345
2346 PendingState::PendingAt(s) => PendingStateMergedPause::PendingAt {
2347 state: s,
2348 paused: false,
2349 },
2350
2351 PendingState::BlockedByJoinSet(s) => PendingStateMergedPause::BlockedByJoinSet {
2352 state: s,
2353 paused: false,
2354 },
2355
2356 PendingState::Paused(paused) => match paused {
2357 PendingStatePaused::PendingAt(s) => PendingStateMergedPause::PendingAt {
2358 state: s,
2359 paused: true,
2360 },
2361 PendingStatePaused::BlockedByJoinSet(s) => {
2362 PendingStateMergedPause::BlockedByJoinSet {
2363 state: s,
2364 paused: true,
2365 }
2366 }
2367 },
2368
2369 PendingState::Finished(s) => PendingStateMergedPause::Finished(s),
2370 }
2371 }
2372}
2373
2374#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2375#[display("Locked(`{lock_expires_at}`, {}, {})", locked_by.executor_id, locked_by.run_id)]
2376pub struct PendingStateLocked {
2377 pub locked_by: LockedBy,
2378 pub lock_expires_at: DateTime<Utc>,
2379}
2380
2381#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2382#[display("`{scheduled_at}`, last_lock={last_lock:?}")]
2383pub struct PendingStatePendingAt {
2384 pub scheduled_at: DateTime<Utc>,
2385 pub last_lock: Option<LockedBy>,
2387}
2388
2389#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2390#[display("{join_set_id}, `{lock_expires_at}`, closing={closing}")]
2391pub struct PendingStateBlockedByJoinSet {
2392 pub join_set_id: JoinSetId,
2393 pub lock_expires_at: DateTime<Utc>,
2395 pub closing: bool,
2397}
2398
2399#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2403pub enum PendingStatePaused {
2404 #[display("PendingAt({_0})")]
2405 PendingAt(PendingStatePendingAt),
2406 #[display("BlockedByJoinSet({_0})")]
2407 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2408}
2409
2410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2411pub struct LockedBy {
2412 pub executor_id: ExecutorId,
2413 pub run_id: RunId,
2414}
2415impl From<&Locked> for LockedBy {
2416 fn from(value: &Locked) -> Self {
2417 LockedBy {
2418 executor_id: value.executor_id,
2419 run_id: value.run_id,
2420 }
2421 }
2422}
2423
2424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2425#[cfg_attr(any(test, feature = "test"), derive(Deserialize))]
2426pub struct PendingStateFinished {
2427 pub version: VersionType, pub finished_at: DateTime<Utc>,
2429 pub result_kind: PendingStateFinishedResultKind,
2430}
2431impl Display for PendingStateFinished {
2432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2433 match self.result_kind {
2434 PendingStateFinishedResultKind::Ok => write!(f, "OK"),
2435 PendingStateFinishedResultKind::Err(err) => write!(f, "{err}"),
2436 }
2437 }
2438}
2439
2440#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2442#[serde(rename_all = "snake_case")]
2443pub enum PendingStateFinishedResultKind {
2444 Ok,
2445 Err(PendingStateFinishedError),
2446}
2447impl PendingStateFinishedResultKind {
2448 pub fn as_result(&self) -> Result<(), &PendingStateFinishedError> {
2449 match self {
2450 PendingStateFinishedResultKind::Ok => Ok(()),
2451 PendingStateFinishedResultKind::Err(err) => Err(err),
2452 }
2453 }
2454}
2455
2456impl From<&SupportedFunctionReturnValue> for PendingStateFinishedResultKind {
2457 fn from(result: &SupportedFunctionReturnValue) -> Self {
2458 result.as_pending_state_finished_result()
2459 }
2460}
2461
2462#[derive(
2463 Debug,
2464 Clone,
2465 Copy,
2466 PartialEq,
2467 Eq,
2468 Serialize,
2469 Deserialize,
2470 derive_more::Display,
2471 schemars::JsonSchema,
2472)]
2473#[serde(rename_all = "snake_case")]
2474pub enum PendingStateFinishedError {
2475 #[display("Execution failure ({_0})")]
2476 ExecutionFailure(ExecutionFailureKind),
2477 #[display("Error")]
2478 Error,
2479}
2480
2481impl PendingState {
2482 #[instrument(skip(self))]
2483 pub fn can_append_lock(
2484 &self,
2485 created_at: DateTime<Utc>,
2486 executor_id: ExecutorId,
2487 run_id: RunId,
2488 lock_expires_at: DateTime<Utc>,
2489 ) -> Result<LockKind, DbErrorWriteNonRetriable> {
2490 if lock_expires_at <= created_at {
2491 return Err(DbErrorWriteNonRetriable::ValidationFailed(
2492 "invalid expiry date".into(),
2493 ));
2494 }
2495 match self {
2496 PendingState::PendingAt(PendingStatePendingAt {
2497 scheduled_at,
2498 last_lock,
2499 }) => {
2500 if *scheduled_at <= created_at {
2501 Ok(LockKind::CreatingNewLock)
2503 } else if let Some(LockedBy {
2504 executor_id: last_executor_id,
2505 run_id: last_run_id,
2506 }) = last_lock
2507 && executor_id == *last_executor_id
2508 && run_id == *last_run_id
2509 {
2510 Ok(LockKind::Extending)
2512 } else {
2513 Err(DbErrorWriteNonRetriable::ValidationFailed(
2514 "cannot lock, not yet pending".into(),
2515 ))
2516 }
2517 }
2518 PendingState::Locked(PendingStateLocked {
2519 locked_by:
2520 LockedBy {
2521 executor_id: current_pending_state_executor_id,
2522 run_id: current_pending_state_run_id,
2523 },
2524 lock_expires_at: _,
2525 }) => {
2526 if executor_id == *current_pending_state_executor_id
2527 && run_id == *current_pending_state_run_id
2528 {
2529 Ok(LockKind::Extending)
2531 } else {
2532 Err(DbErrorWriteNonRetriable::IllegalState {
2533 reason: "cannot lock, already locked".into(),
2534 context: SpanTrace::capture(),
2535 source: None,
2536 loc: Location::caller(),
2537 })
2538 }
2539 }
2540 PendingState::BlockedByJoinSet { .. } => Err(DbErrorWriteNonRetriable::IllegalState {
2541 reason: "cannot append Locked event when in BlockedByJoinSet state".into(),
2542 context: SpanTrace::capture(),
2543 source: None,
2544 loc: Location::caller(),
2545 }),
2546 PendingState::Finished { .. } => Err(DbErrorWriteNonRetriable::IllegalState {
2547 reason: "already finished".into(),
2548 context: SpanTrace::capture(),
2549 source: None,
2550 loc: Location::caller(),
2551 }),
2552 PendingState::Paused(..) => Err(DbErrorWriteNonRetriable::IllegalState {
2553 reason: "cannot lock, execution is paused".into(),
2554 context: SpanTrace::capture(),
2555 source: None,
2556 loc: Location::caller(),
2557 }),
2558 }
2559 }
2560
2561 #[must_use]
2562 pub fn is_finished(&self) -> bool {
2563 matches!(self, PendingState::Finished { .. })
2564 }
2565
2566 #[must_use]
2567 pub fn is_paused(&self) -> bool {
2568 matches!(self, PendingState::Paused(_))
2569 }
2570}
2571
2572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2573pub enum LockKind {
2574 Extending,
2575 CreatingNewLock,
2576}
2577
2578pub mod http_client_trace {
2579 use chrono::{DateTime, Utc};
2580 use serde::{Deserialize, Serialize};
2581
2582 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2583 pub struct HttpClientTrace {
2584 pub req: RequestTrace,
2585 pub resp: Option<ResponseTrace>,
2586 }
2587
2588 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2589 pub struct RequestTrace {
2590 pub sent_at: DateTime<Utc>,
2591 pub uri: String,
2592 pub method: String,
2593 }
2594
2595 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2596 pub struct ResponseTrace {
2597 pub finished_at: DateTime<Utc>,
2598 pub status: Result<u16, String>,
2599 }
2600}
2601
2602#[derive(schemars::JsonSchema)]
2604pub struct DbStorageSchema {
2605 pub execution_event: ExecutionEvent,
2606 pub pending_state: PendingState,
2607 pub join_set_response: JoinSetResponse,
2608 pub wasm_backtrace: WasmBacktrace,
2609 pub persisted_function_metadata: PersistedFunctionMetadata,
2610}
2611
2612#[cfg(test)]
2613mod tests {
2614 use super::HistoryEvent;
2615 use super::HistoryEventScheduleAt;
2616 use super::JoinNextTryOutcome;
2617 use super::PendingStateFinished;
2618 use super::PendingStateFinishedError;
2619 use super::PendingStateFinishedResultKind;
2620 use crate::ExecutionFailureKind;
2621 use crate::JoinSetId;
2622 use crate::SupportedFunctionReturnValue;
2623 use chrono::DateTime;
2624 use chrono::Datelike;
2625 use chrono::Utc;
2626 use insta::assert_snapshot;
2627 use rstest::rstest;
2628 use std::time::Duration;
2629 use val_json::type_wrapper::TypeWrapper;
2630 use val_json::wast_val::WastVal;
2631 use val_json::wast_val::WastValWithType;
2632
2633 #[rstest(expected => [
2634 PendingStateFinishedResultKind::Ok,
2635 PendingStateFinishedResultKind::Err(PendingStateFinishedError::ExecutionFailure(ExecutionFailureKind::TimedOut)),
2636 ])]
2637 #[test]
2638 fn serde_pending_state_finished_result_kind_should_work(
2639 expected: PendingStateFinishedResultKind,
2640 ) {
2641 let ser = serde_json::to_string(&expected).unwrap();
2642 let actual: PendingStateFinishedResultKind = serde_json::from_str(&ser).unwrap();
2643 assert_eq!(expected, actual);
2644 }
2645
2646 #[rstest(result_kind => [
2647 PendingStateFinishedResultKind::Ok,
2648 PendingStateFinishedResultKind::Err(PendingStateFinishedError::ExecutionFailure(ExecutionFailureKind::TimedOut)),
2649 ])]
2650 #[test]
2651 fn serde_pending_state_finished_should_work(result_kind: PendingStateFinishedResultKind) {
2652 let expected = PendingStateFinished {
2653 version: 0,
2654 finished_at: Utc::now(),
2655 result_kind,
2656 };
2657
2658 let ser = serde_json::to_string(&expected).unwrap();
2659 let actual: PendingStateFinished = serde_json::from_str(&ser).unwrap();
2660 assert_eq!(expected, actual);
2661 }
2662
2663 #[test]
2664 fn join_set_deser_with_result_ok_option_none_should_work() {
2665 let expected = SupportedFunctionReturnValue::Ok(Some(WastValWithType {
2666 r#type: TypeWrapper::Result {
2667 ok: Some(Box::new(TypeWrapper::Option(Box::new(TypeWrapper::String)))),
2668 err: Some(Box::new(TypeWrapper::String)),
2669 },
2670 value: WastVal::Result(Ok(Some(Box::new(WastVal::Option(None))))),
2671 }));
2672 let json = serde_json::to_string(&expected).unwrap();
2673 assert_snapshot!(json);
2674
2675 let actual: SupportedFunctionReturnValue = serde_json::from_str(&json).unwrap();
2676
2677 assert_eq!(expected, actual);
2678 }
2679
2680 #[test]
2681 fn as_date_time_should_work_with_duration_u32_max_secs() {
2682 let duration = Duration::from_secs(u64::from(u32::MAX));
2683 let schedule_at = HistoryEventScheduleAt::In(duration);
2684 let resolved = schedule_at.as_date_time(DateTime::UNIX_EPOCH).unwrap();
2685 assert_eq!(2106, resolved.year());
2686 }
2687
2688 const MILLIS_PER_SEC: i64 = 1000;
2689 const TIMEDELTA_MAX_SECS: i64 = i64::MAX / MILLIS_PER_SEC;
2690
2691 #[test]
2692 fn as_date_time_should_fail_on_duration_secs_greater_than_i64_max() {
2693 let duration = Duration::from_secs(
2695 u64::try_from(TIMEDELTA_MAX_SECS).expect("positive number must not fail") + 1,
2696 );
2697 let schedule_at = HistoryEventScheduleAt::In(duration);
2698 schedule_at.as_date_time(DateTime::UNIX_EPOCH).unwrap_err();
2699 }
2700
2701 #[test]
2702 fn join_next_try_outcome_new_format() {
2703 let json = r#"{"type":"join_next_try","join_set_id":"n:test","outcome":"found"}"#;
2704 let event: HistoryEvent = serde_json::from_str(json).unwrap();
2705 assert_eq!(
2706 event,
2707 HistoryEvent::JoinNextTry {
2708 join_set_id: JoinSetId::new(
2709 crate::JoinSetKind::Named,
2710 crate::StrVariant::Static("test")
2711 )
2712 .unwrap(),
2713 outcome: JoinNextTryOutcome::Found,
2714 }
2715 );
2716
2717 let json = r#"{"type":"join_next_try","join_set_id":"n:test","outcome":"all_processed"}"#;
2718 let event: HistoryEvent = serde_json::from_str(json).unwrap();
2719 assert_eq!(
2720 event,
2721 HistoryEvent::JoinNextTry {
2722 join_set_id: JoinSetId::new(
2723 crate::JoinSetKind::Named,
2724 crate::StrVariant::Static("test")
2725 )
2726 .unwrap(),
2727 outcome: JoinNextTryOutcome::AllProcessed,
2728 }
2729 );
2730 }
2731
2732 #[test]
2733 fn join_next_try_outcome_serializes_new_format() {
2734 let event = HistoryEvent::JoinNextTry {
2735 join_set_id: JoinSetId::new(
2736 crate::JoinSetKind::Named,
2737 crate::StrVariant::Static("test"),
2738 )
2739 .unwrap(),
2740 outcome: JoinNextTryOutcome::AllProcessed,
2741 };
2742 let json = serde_json::to_string(&event).unwrap();
2743 assert!(
2744 json.contains(r#""outcome":"all_processed""#),
2745 "expected outcome field, got: {json}"
2746 );
2747 assert!(
2748 !json.contains("found_response"),
2749 "should not contain old field, got: {json}"
2750 );
2751 }
2752
2753 mod stub_retval_hash {
2754 use super::super::{StubRetVal, StubRetValHash};
2755 use crate::SupportedFunctionReturnValue;
2756 use val_json::type_wrapper::TypeWrapper;
2757 use val_json::wast_val::{WastVal, WastValWithType};
2758
2759 #[test]
2760 fn typed_variant_hash_is_stable() {
2761 let retval =
2762 StubRetVal::Typed(SupportedFunctionReturnValue::Ok(Some(WastValWithType {
2763 r#type: TypeWrapper::String,
2764 value: WastVal::String("hello".into()),
2765 })));
2766 let hash = retval.hash();
2767 assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
2769 assert_eq!(hash.to_string().len(), 66);
2771 }
2772
2773 #[test]
2774 fn untyped_variant_hash_is_stable() {
2775 let retval = StubRetVal::Untyped(r#"{"ok": "hello"}"#.to_string());
2776 let hash = retval.hash();
2777 assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
2779 assert_eq!(hash.to_string().len(), 66);
2781 }
2782
2783 #[test]
2784 fn different_values_produce_different_hashes() {
2785 let typed1 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
2786 let typed2 = StubRetVal::Typed(SupportedFunctionReturnValue::Err(None));
2787 let untyped1 = StubRetVal::Untyped("value1".to_string());
2788 let untyped2 = StubRetVal::Untyped("value2".to_string());
2789
2790 let hashes: Vec<_> = [typed1, typed2, untyped1, untyped2]
2791 .into_iter()
2792 .map(|r| r.hash().to_string())
2793 .collect();
2794
2795 for (i, h1) in hashes.iter().enumerate() {
2797 for h2 in hashes.iter().skip(i + 1) {
2798 assert_ne!(h1, h2, "hashes should be different");
2799 }
2800 }
2801 }
2802
2803 #[test]
2804 fn same_values_produce_same_hashes() {
2805 let retval1 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
2806 let retval2 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
2807 assert_eq!(retval1.hash(), retval2.hash());
2808
2809 let untyped1 = StubRetVal::Untyped("test".to_string());
2810 let untyped2 = StubRetVal::Untyped("test".to_string());
2811 assert_eq!(untyped1.hash(), untyped2.hash());
2812 }
2813
2814 #[test]
2815 fn hash_serialization_roundtrip() {
2816 let retval = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
2817 let hash = retval.hash();
2818
2819 let serialized = serde_json::to_string(&hash).unwrap();
2820 let deserialized: StubRetValHash = serde_json::from_str(&serialized).unwrap();
2821
2822 assert_eq!(hash, deserialized);
2823 }
2824
2825 #[test]
2826 fn hash_display_and_fromstr_roundtrip() {
2827 let retval = StubRetVal::Untyped("test value".to_string());
2828 let hash = retval.hash();
2829
2830 let display = hash.to_string();
2831 let parsed: StubRetValHash = display.parse().unwrap();
2832
2833 assert_eq!(hash, parsed);
2834 }
2835
2836 #[test]
2837 fn typed_and_untyped_with_same_content_produce_different_hashes() {
2838 let typed = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
2840 let json_of_typed =
2841 serde_json::to_string(&SupportedFunctionReturnValue::Ok(None)).unwrap();
2842 let untyped = StubRetVal::Untyped(json_of_typed);
2843
2844 assert_ne!(typed.hash(), untyped.hash());
2845 }
2846 }
2847}