1use crate::ComponentId;
2use crate::ComponentRetryConfig;
3use crate::ComponentType;
4use crate::ContentDigest;
5use crate::ExecutionFailureKind;
6use crate::ExecutionId;
7use crate::ExecutionMetadata;
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::instrument;
35use tracing_error::SpanTrace;
36
37pub const STATE_PENDING_AT: &str = "pending_at";
39pub const STATE_BLOCKED_BY_JOIN_SET: &str = "blocked_by_join_set";
40pub const STATE_LOCKED: &str = "locked";
41pub const STATE_FINISHED: &str = "finished";
42pub const LIFECYCLE_ACTIVE: &str = "active";
45pub const LIFECYCLE_PAUSED: &str = "paused";
46pub const LIFECYCLE_CANCELLING: &str = "cancelling";
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Lifecycle {
52 Active,
53 Paused,
54 Cancelling,
55}
56impl Lifecycle {
57 #[must_use]
58 pub fn as_column(self) -> &'static str {
59 match self {
60 Lifecycle::Active => LIFECYCLE_ACTIVE,
61 Lifecycle::Paused => LIFECYCLE_PAUSED,
62 Lifecycle::Cancelling => LIFECYCLE_CANCELLING,
63 }
64 }
65 #[must_use]
66 pub fn from_column(column: &str) -> Option<Self> {
67 match column {
68 LIFECYCLE_ACTIVE => Some(Lifecycle::Active),
69 LIFECYCLE_PAUSED => Some(Lifecycle::Paused),
70 LIFECYCLE_CANCELLING => Some(Lifecycle::Cancelling),
71 _ => None,
72 }
73 }
74}
75pub const RESULT_KIND_JSON_OK: &str = r#""ok""#;
78pub const RESULT_KIND_JSON_ERROR: &str = r#"{"err":"error"}"#;
79pub const HISTORY_EVENT_TYPE_JOIN_NEXT: &str = "join_next"; #[derive(Debug, PartialEq, Eq, Clone)]
82pub struct ExecutionLog {
83 pub execution_id: ExecutionId,
84 pub events: Vec<ExecutionEvent>,
85 pub responses: Vec<ResponseWithCursor>,
86 pub next_version: Version, pub pending_state: PendingState, pub component_digest: ComponentDigest, pub component_type: ComponentType,
90 pub deployment_id: DeploymentId, }
92
93impl ExecutionLog {
94 #[must_use]
97 pub fn can_be_retried_after(
98 temporary_event_count: u32,
99 max_retries: Option<u32>,
100 retry_exp_backoff: Duration,
101 ) -> Option<Duration> {
102 if temporary_event_count <= max_retries.unwrap_or(u32::MAX) {
104 let duration = retry_exp_backoff * 2_u32.saturating_pow(temporary_event_count - 1);
106 Some(duration)
107 } else {
108 None
109 }
110 }
111
112 #[must_use]
113 pub fn compute_retry_duration_when_retrying_forever(
114 temporary_event_count: u32,
115 retry_exp_backoff: Duration,
116 ) -> Duration {
117 Self::can_be_retried_after(temporary_event_count, None, retry_exp_backoff)
118 .expect("`max_retries` set to MAX must never return None")
119 }
120
121 #[must_use]
122 pub fn get_create_request(&self) -> CreateRequest {
123 assert_matches!(self.events.first().cloned(), Some(ExecutionEvent {
124 event:ExecutionRequest::Created{
125 ffqn,params,parent,scheduled_at,component_id,deployment_id,metadata,scheduled_by},
126 created_at, .. }) => CreateRequest { created_at, execution_id:
127 self.execution_id.clone(), ffqn, params, parent, scheduled_at,
128 component_id, deployment_id, metadata, scheduled_by, paused: false })
129 }
130
131 #[must_use]
132 pub fn ffqn(&self) -> &FunctionFqn {
133 assert_matches!(self.events.first(), Some(ExecutionEvent {
134 event: ExecutionRequest::Created { ffqn, .. },
135 ..
136 }) => ffqn)
137 }
138
139 #[must_use]
140 pub fn params(&self) -> &Params {
141 assert_matches!(self.events.first(), Some(ExecutionEvent {
142 event: ExecutionRequest::Created { params, .. },
143 ..
144 }) => params)
145 }
146
147 #[must_use]
148 pub fn parent(&self) -> Option<(ExecutionId, JoinSetId)> {
149 assert_matches!(self.events.first(), Some(ExecutionEvent {
150 event: ExecutionRequest::Created { parent, .. },
151 ..
152 }) => parent.clone())
153 }
154
155 #[must_use]
156 pub fn last_event(&self) -> &ExecutionEvent {
157 self.events.last().expect("must contain at least one event")
158 }
159
160 #[must_use]
161 pub fn is_finished(&self) -> bool {
162 matches!(
163 self.events.last(),
164 Some(ExecutionEvent {
165 event: ExecutionRequest::Finished { .. },
166 ..
167 })
168 )
169 }
170
171 #[must_use]
172 pub fn as_finished_result(&self) -> Option<SupportedFunctionReturnValue> {
173 if let ExecutionEvent {
174 event: ExecutionRequest::Finished { retval: result, .. },
175 ..
176 } = self.events.last().expect("must contain at least one event")
177 {
178 Some(result.clone())
179 } else {
180 None
181 }
182 }
183
184 pub fn event_history(&self) -> impl Iterator<Item = (HistoryEvent, Version)> + '_ {
185 self.events.iter().filter_map(|event| {
186 if let ExecutionRequest::HistoryEvent { event: eh, .. } = &event.event {
187 Some((eh.clone(), event.version.clone()))
188 } else {
189 None
190 }
191 })
192 }
193
194 #[cfg(feature = "test")]
195 #[must_use]
196 pub fn find_join_set_request(&self, join_set_id: &JoinSetId) -> Option<&JoinSetRequest> {
197 self.events
198 .iter()
199 .find_map(move |event| match &event.event {
200 ExecutionRequest::HistoryEvent {
201 event:
202 HistoryEvent::JoinSetRequest {
203 join_set_id: found,
204 request,
205 },
206 ..
207 } if *join_set_id == *found => Some(request),
208 _ => None,
209 })
210 }
211}
212
213pub type VersionType = u32;
214#[derive(
215 Debug,
216 Default,
217 Clone,
218 PartialEq,
219 PartialOrd,
220 Ord,
221 Eq,
222 Hash,
223 derive_more::Display,
224 derive_more::Into,
225 serde::Serialize,
226 serde::Deserialize,
227 schemars::JsonSchema,
228)]
229#[serde(transparent)]
230#[schemars(transparent)]
231pub struct Version(pub VersionType);
232impl Version {
233 #[must_use]
234 pub fn new(arg: VersionType) -> Version {
235 Version(arg)
236 }
237
238 #[must_use]
239 pub fn increment(&self) -> Version {
240 Version(self.0 + 1)
241 }
242}
243impl TryFrom<i64> for Version {
244 type Error = VersionParseError;
245 fn try_from(value: i64) -> Result<Self, Self::Error> {
246 VersionType::try_from(value)
247 .map(Version::new)
248 .map_err(|_| VersionParseError)
249 }
250}
251impl From<Version> for usize {
252 fn from(value: Version) -> Self {
253 usize::try_from(value.0).expect("16 bit systems are unsupported")
254 }
255}
256impl From<&Version> for usize {
257 fn from(value: &Version) -> Self {
258 usize::try_from(value.0).expect("16 bit systems are unsupported")
259 }
260}
261
262#[derive(Debug, thiserror::Error)]
263#[error("version must be u32")]
264pub struct VersionParseError;
265
266#[derive(
267 Clone,
268 Debug,
269 derive_more::Display,
270 PartialEq,
271 Eq,
272 serde::Serialize,
273 serde::Deserialize,
274 schemars::JsonSchema,
275)]
276#[display("{event}")]
277pub struct ExecutionEvent {
278 pub created_at: DateTime<Utc>,
279 pub event: ExecutionRequest,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub backtrace_id: Option<Version>,
282 pub version: Version,
283}
284
285#[derive(
286 Debug,
287 Clone,
288 Copy,
289 PartialEq,
290 Eq,
291 derive_more::Display,
292 derive_more::Into,
293 Serialize, schemars::JsonSchema,
295)]
296pub struct ResponseCursor(pub u32);
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize , schemars::JsonSchema)]
299pub struct ResponseWithCursor {
300 pub event: JoinSetResponseEventOuter,
301 pub cursor: ResponseCursor,
302}
303
304#[derive(Debug)]
305pub struct ListExecutionEventsResponse {
306 pub events: Vec<ExecutionEvent>,
307 pub max_version: Version,
308}
309
310#[derive(Debug)]
311pub struct ListResponsesResponse {
312 pub responses: Vec<ResponseWithCursor>,
313 pub max_cursor: ResponseCursor,
314 pub scan_cursor: ResponseCursor,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Serialize , schemars::JsonSchema)]
318pub struct JoinSetResponseEventOuter {
319 pub created_at: DateTime<Utc>,
320 pub event: JoinSetResponseEvent,
321}
322
323#[derive(
324 Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
325)]
326pub struct JoinSetResponseEvent {
327 pub join_set_id: JoinSetId,
328 pub event: JoinSetResponse,
329}
330
331#[derive(
332 Clone, Debug, PartialEq, Eq, Serialize, Deserialize, derive_more::Display, schemars::JsonSchema,
333)]
334#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
335#[serde(tag = "type", rename_all = "snake_case")]
336pub enum JoinSetResponse {
337 #[display("delay {}: {delay_id}", if result.is_ok() { "finished" } else { "cancelled"})]
338 DelayFinished {
339 delay_id: DelayId,
340 result: Result<(), ()>,
341 },
342 #[display("{result}: {child_execution_id}")] ChildExecutionFinished {
344 child_execution_id: ExecutionIdDerived,
345 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Version(2)))]
346 finished_version: Version,
347 #[cfg_attr(any(test, feature = "test"), arbitrary(value = crate::SUPPORTED_RETURN_VALUE_OK_EMPTY))]
348 result: SupportedFunctionReturnValue,
349 },
350}
351
352pub const DUMMY_CREATED: ExecutionRequest = ExecutionRequest::Created {
353 ffqn: FunctionFqn::new_static("", ""),
354 params: Params::empty(),
355 parent: None,
356 scheduled_at: DateTime::from_timestamp_nanos(0),
357 component_id: ComponentId::dummy_activity(),
358 deployment_id: DeploymentId::from_parts(0, 0),
359 metadata: ExecutionMetadata::empty(),
360 scheduled_by: None,
361};
362pub const DUMMY_HISTORY_EVENT: ExecutionRequest = ExecutionRequest::HistoryEvent {
363 event: HistoryEvent::JoinSetCreate {
364 join_set_id: JoinSetId {
365 kind: crate::JoinSetKind::OneOff,
366 name: StrVariant::empty(),
367 },
368 },
369};
370
371#[derive(
372 Clone,
373 derive_more::Debug,
374 derive_more::Display,
375 PartialEq,
376 Eq,
377 Serialize,
378 Deserialize,
379 schemars::JsonSchema,
380)]
381#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
382#[serde(rename_all = "snake_case")]
383pub enum ExecutionRequest {
384 #[display("Created({ffqn}, `{scheduled_at}`)")]
385 Created {
386 ffqn: FunctionFqn,
387 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Params::empty()))]
388 #[debug(skip)]
389 params: Params,
390 parent: Option<(ExecutionId, JoinSetId)>,
391 scheduled_at: DateTime<Utc>,
392 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity()))]
393 component_id: ComponentId,
394 deployment_id: DeploymentId,
395 #[cfg_attr(any(test, feature = "test"), arbitrary(default))]
396 metadata: ExecutionMetadata,
397 scheduled_by: Option<ExecutionId>,
398 },
399 Locked(Locked),
400 #[display("Unlocked({_0})")]
409 Unlocked(Unlocked),
410 #[display("ComponentUpgradeFinished({component_digest})")]
412 ComponentUpgradeFinished {
413 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity().component_digest))]
414 component_digest: ComponentDigest,
415 #[cfg_attr(any(test, feature = "test"), arbitrary(value = DeploymentId::from_parts(0, 0)))]
416 deployment_id: DeploymentId,
417 outcome: ComponentUpgradeOutcome,
418 },
419 #[display("TemporarilyFailed(`{backoff_expires_at}`)")]
422 TemporarilyFailed {
423 backoff_expires_at: DateTime<Utc>,
424 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
425 reason: StrVariant,
426 detail: Option<String>,
427 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
428 http_client_traces: Option<Vec<HttpClientTrace>>,
429 },
430 #[display("TemporarilyTimedOut(`{backoff_expires_at}`)")]
433 TemporarilyTimedOut {
434 backoff_expires_at: DateTime<Utc>,
435 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
436 http_client_traces: Option<Vec<HttpClientTrace>>,
437 },
438 #[display("Finished: {retval}")]
440 Finished {
441 #[cfg_attr(any(test, feature = "test"), arbitrary(value = crate::SUPPORTED_RETURN_VALUE_OK_EMPTY))]
442 retval: SupportedFunctionReturnValue,
443 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
444 http_client_traces: Option<Vec<HttpClientTrace>>,
445 },
446
447 #[display("HistoryEvent({event})")]
448 HistoryEvent {
449 event: HistoryEvent,
450 },
451 #[display("Paused")]
452 Paused,
453 #[display("Unpaused")]
454 Unpaused,
455 #[display("CancellationRequested")]
466 CancellationRequested,
467}
468
469#[derive(
471 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
472)]
473#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
474#[serde(tag = "type", rename_all = "snake_case")]
475pub enum ComponentUpgradeReason {
476 #[display("auto")]
477 Auto,
478 #[display("manual(force = {force})")]
479 Manual { force: bool },
480}
481
482#[derive(
483 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
484)]
485#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
486#[display("{reason}, pending at {unlocked_at}")]
487pub struct Unlocked {
488 #[serde(rename = "backoff_expires_at")]
491 pub unlocked_at: DateTime<Utc>,
492 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
493 pub reason: StrVariant,
494}
495
496#[derive(
497 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
498)]
499#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
500#[serde(tag = "type", rename_all = "snake_case")]
501pub enum ComponentUpgradeOutcome {
502 #[display("success({reason})")]
503 Success { reason: ComponentUpgradeReason },
504 #[display("failed: {reason}")]
505 Failed {
506 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
507 reason: StrVariant,
508 },
509}
510
511impl ExecutionRequest {
512 #[must_use]
513 pub fn is_temporary_event(&self) -> bool {
514 matches!(
515 self,
516 Self::TemporarilyFailed { .. } | Self::TemporarilyTimedOut { .. }
517 )
518 }
519
520 #[must_use]
522 pub const fn variant(&self) -> &'static str {
523 match self {
524 ExecutionRequest::Created { .. } => "created",
525 ExecutionRequest::Locked(_) => "locked",
526 ExecutionRequest::Unlocked(_) => "unlocked",
527 ExecutionRequest::ComponentUpgradeFinished { .. } => "component_upgrade_finished",
528 ExecutionRequest::TemporarilyFailed { .. } => "temporarily_failed",
529 ExecutionRequest::TemporarilyTimedOut { .. } => "temporarily_timed_out",
530 ExecutionRequest::Finished { .. } => "finished",
531 ExecutionRequest::HistoryEvent { .. } => "history_event",
532 ExecutionRequest::Paused => "paused",
533 ExecutionRequest::Unpaused => "unpaused",
534 ExecutionRequest::CancellationRequested => "cancellation_requested",
535 }
536 }
537
538 #[must_use]
539 pub fn join_set_id(&self) -> Option<&JoinSetId> {
540 match self {
541 Self::Created {
542 parent: Some((_parent_id, join_set_id)),
543 ..
544 } => Some(join_set_id),
545 Self::HistoryEvent {
546 event:
547 HistoryEvent::JoinSetCreate { join_set_id, .. }
548 | HistoryEvent::JoinSetRequest { join_set_id, .. }
549 | HistoryEvent::JoinNext { join_set_id, .. },
550 } => Some(join_set_id),
551 _ => None,
552 }
553 }
554}
555
556#[derive(
557 Clone,
558 derive_more::Debug,
559 derive_more::Display,
560 PartialEq,
561 Eq,
562 Serialize,
563 Deserialize,
564 schemars::JsonSchema,
565)]
566#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
567#[display("Locked(`{lock_expires_at}`, {component_id})")]
568pub struct Locked {
569 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity()))]
570 pub component_id: ComponentId,
571 pub executor_id: ExecutorId,
572 pub deployment_id: DeploymentId,
573 pub run_id: RunId,
574 pub lock_expires_at: DateTime<Utc>,
575 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentRetryConfig::ZERO))]
576 pub retry_config: ComponentRetryConfig,
577}
578
579#[derive(
580 Debug,
581 Clone,
582 Copy,
583 PartialEq,
584 Eq,
585 derive_more::Display,
586 Serialize,
587 Deserialize,
588 schemars::JsonSchema,
589)]
590#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
591#[serde(tag = "type", rename_all = "snake_case")]
592pub enum PersistKind {
593 #[display("RandomU64({min}, {max_inclusive})")]
594 RandomU64 {
595 min: u64,
596 max_inclusive: u64,
597 },
598 #[display("RandomString({min_length}, {max_length_exclusive})")]
599 RandomString {
600 min_length: u64,
601 max_length_exclusive: u64,
602 },
603 ExecutionId,
604}
605
606#[must_use]
607pub fn from_u64_to_bytes(value: u64) -> [u8; 8] {
608 value.to_be_bytes()
609}
610
611#[derive(
612 derive_more::Debug,
613 Clone,
614 PartialEq,
615 Eq,
616 derive_more::Display,
617 Serialize,
618 Deserialize,
619 schemars::JsonSchema,
620)]
621#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
622#[serde(tag = "type", rename_all = "snake_case")]
623pub enum HistoryEvent {
625 #[display("Persist")]
627 Persist {
628 #[debug(skip)]
629 value: Vec<u8>, kind: PersistKind,
631 },
632 #[display("JoinSetCreate({join_set_id})")]
633 JoinSetCreate { join_set_id: JoinSetId },
634 #[display("JoinSetRequest({request})")]
635 JoinSetRequest {
637 join_set_id: JoinSetId,
638 request: JoinSetRequest,
639 },
640 #[display("JoinNext({join_set_id})")]
646 JoinNext {
647 join_set_id: JoinSetId,
648 run_expires_at: DateTime<Utc>,
651 requested_ffqn: Option<FunctionFqn>,
654 closing: bool,
656 },
657 #[display("JoinNextTry({join_set_id}, {outcome})")]
659 JoinNextTry {
660 join_set_id: JoinSetId,
661 outcome: JoinNextTryOutcome,
662 },
663 #[display("JoinNextTooMany({join_set_id})")]
665 JoinNextTooMany {
666 join_set_id: JoinSetId,
667 requested_ffqn: Option<FunctionFqn>,
670 },
671 #[display("Schedule({execution_id}, {schedule_at})")]
672 Schedule {
673 execution_id: ExecutionId,
674 schedule_at: HistoryEventScheduleAt, #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
676 result: Result<(), ScheduleRequestError>,
677 },
678 #[display("Stub({target_execution_id})")]
679 Stub {
680 target_execution_id: ExecutionIdDerived,
681 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StubRetVal::Typed(crate::SUPPORTED_RETURN_VALUE_OK_EMPTY).hash()))]
682 retval_hash: StubRetValHash,
683 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
684 result: Result<(), StubError>,
685 },
686}
687
688#[derive(derive_more::Debug, Clone, PartialEq, Eq)]
690#[cfg_attr(any(test, feature = "test"), derive(Serialize, Deserialize))]
691#[cfg_attr(any(test, feature = "test"), serde(rename_all = "snake_case"))]
692pub enum StubRetVal {
693 Typed(SupportedFunctionReturnValue),
694 Untyped(String),
695}
696
697impl StubRetVal {
698 #[must_use]
700 pub fn hash(&self) -> StubRetValHash {
701 use sha2::{Digest as _, Sha256};
702 const STUB_RETVAL_HASH_VERSION: u8 = 1;
703 let mut hasher = Sha256::default();
704
705 match self {
706 StubRetVal::Typed(val) => {
707 hasher.update(b"T|");
708 let json = serde_json::to_string(val)
710 .expect("SupportedFunctionReturnValue is always serializable");
711 hasher.update(json.as_bytes());
712 }
713 StubRetVal::Untyped(s) => {
714 hasher.update(b"U|");
715 hasher.update(s.as_bytes());
716 }
717 }
718
719 let hash_bytes = hasher.finalize();
720 let mut result = [0u8; 33];
721 result[0] = STUB_RETVAL_HASH_VERSION;
722 result[1..].copy_from_slice(&hash_bytes);
723
724 StubRetValHash(result)
725 }
726}
727
728#[derive(
731 Clone,
732 PartialEq,
733 Eq,
734 serde_with::SerializeDisplay,
735 serde_with::DeserializeFromStr,
736 schemars::JsonSchema,
737)]
738#[schemars(with = "String")]
739pub struct StubRetValHash([u8; 33]);
740
741impl Display for StubRetValHash {
742 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
743 for b in self.0 {
744 write!(f, "{b:02x}")?;
745 }
746 Ok(())
747 }
748}
749
750impl Debug for StubRetValHash {
751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
752 Display::fmt(self, f)
753 }
754}
755
756impl std::str::FromStr for StubRetValHash {
757 type Err = StubRetValHashParseError;
758
759 fn from_str(s: &str) -> Result<Self, Self::Err> {
760 if s.len() != 66 {
761 return Err(StubRetValHashParseError::InvalidLength(s.len()));
763 }
764 let mut bytes = [0u8; 33];
765 for i in 0..33 {
766 let chunk = &s[i * 2..i * 2 + 2];
767 bytes[i] =
768 u8::from_str_radix(chunk, 16).map_err(|_| StubRetValHashParseError::InvalidHex)?;
769 }
770 Ok(StubRetValHash(bytes))
771 }
772}
773
774#[derive(Debug, thiserror::Error)]
775pub enum StubRetValHashParseError {
776 #[error("invalid length: expected 66 hex chars, got {0}")]
777 InvalidLength(usize),
778 #[error("invalid hex character")]
779 InvalidHex,
780}
781
782#[derive(
785 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
786)]
787#[serde(rename_all = "snake_case")]
788pub enum StubError {
789 #[error("execution not found")]
790 ExecutionNotFound,
791 #[error("type check error: {0}")]
792 TypeCheckError(String),
793 #[error("conflict")]
794 Conflict,
795}
796
797#[derive(
799 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
800)]
801#[serde(rename_all = "snake_case")]
802pub enum ScheduleRequestError {
803 #[error("function not found")]
804 FunctionNotFound,
805 #[error("params parsing error: {0}")]
806 TypeCheckError(String),
807}
808
809#[derive(
811 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
812)]
813#[serde(rename_all = "snake_case")]
814pub enum ChildExecutionRequestError {
815 #[error("function not found")]
816 FunctionNotFound,
817 #[error("params parsing error: {0}")]
818 TypeCheckError(String),
819}
820
821#[derive(
822 Debug,
823 Clone,
824 Copy,
825 PartialEq,
826 Eq,
827 derive_more::Display,
828 Serialize,
829 Deserialize,
830 schemars::JsonSchema,
831)]
832#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
833#[serde(rename_all = "snake_case")]
834pub enum JoinNextTryOutcome {
835 #[display("found")]
837 Found,
838 #[display("pending")]
840 Pending,
841 #[display("all_processed")]
843 AllProcessed,
844}
845
846impl From<bool> for JoinNextTryOutcome {
847 fn from(found_response: bool) -> Self {
851 if found_response {
852 JoinNextTryOutcome::Found
853 } else {
854 JoinNextTryOutcome::Pending
855 }
856 }
857}
858
859#[derive(
860 Debug,
861 Clone,
862 Copy,
863 PartialEq,
864 Eq,
865 derive_more::Display,
866 Serialize,
867 Deserialize,
868 schemars::JsonSchema,
869)]
870#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
871#[serde(rename_all = "snake_case")]
872pub enum HistoryEventScheduleAt {
873 Now,
874 #[display("At(`{_0}`)")]
875 At(DateTime<Utc>),
876 #[display("In({_0:?})")]
877 In(Duration),
878}
879
880#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
881pub enum ScheduleAtConversionError {
882 #[error("source duration value is out of range")]
883 OutOfRangeError,
884}
885
886impl HistoryEventScheduleAt {
887 pub fn as_date_time(
888 &self,
889 now: DateTime<Utc>,
890 ) -> Result<DateTime<Utc>, ScheduleAtConversionError> {
891 match self {
892 Self::Now => Ok(now),
893 Self::At(date_time) => Ok(*date_time),
894 Self::In(duration) => {
895 let time_delta = TimeDelta::from_std(*duration)
896 .map_err(|_| ScheduleAtConversionError::OutOfRangeError)?;
897 now.checked_add_signed(time_delta)
898 .ok_or(ScheduleAtConversionError::OutOfRangeError)
899 }
900 }
901 }
902}
903
904#[derive(
905 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
906)]
907#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
908#[serde(tag = "type", rename_all = "snake_case")]
909pub enum JoinSetRequest {
910 #[display("DelayRequest({delay_id}, expires_at: `{expires_at}`, schedule_at: `{schedule_at}`)")]
912 DelayRequest {
913 delay_id: DelayId,
914 expires_at: DateTime<Utc>,
915 schedule_at: HistoryEventScheduleAt,
916 #[serde(default)]
917 paused: bool,
918 },
919 #[display("ChildExecutionRequest({child_execution_id}, {target_ffqn}, params: {params})")]
921 ChildExecutionRequest {
922 child_execution_id: ExecutionIdDerived,
923 target_ffqn: FunctionFqn,
924 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Params::empty()))]
925 params: Params,
926 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
927 result: Result<(), ChildExecutionRequestError>,
928 },
929}
930
931#[derive(Debug, Clone, thiserror::Error, derive_more::PartialEq, derive_more::Eq)]
933pub enum DbErrorGeneric {
934 #[error("database error: {reason}")]
935 Uncategorized {
936 reason: StrVariant,
937 #[eq(skip)]
938 #[partial_eq(skip)]
939 context: SpanTrace,
940 #[eq(skip)]
941 #[partial_eq(skip)]
942 #[source]
943 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
944 loc: &'static Location<'static>,
945 },
946 #[error("database was closed")]
947 Close,
948}
949
950#[derive(thiserror::Error, Clone, Debug, derive_more::PartialEq, derive_more::Eq)]
951pub enum DbErrorWriteNonRetriable {
952 #[error("validation failed: {0}")]
953 ValidationFailed(StrVariant),
954 #[error("conflict")]
955 Conflict,
956 #[error("already finished")]
957 AlreadyFinished,
958 #[error("illegal state: {reason}")]
959 IllegalState {
960 reason: StrVariant,
961 #[eq(skip)]
962 #[partial_eq(skip)]
963 context: SpanTrace,
964 #[eq(skip)]
965 #[partial_eq(skip)]
966 #[source]
967 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
968 loc: &'static Location<'static>,
969 },
970 #[error("illegal state: `Unlocked` cannot be appended in state {0}")]
971 UnlockedCannotBeAppended(&'static str),
972 #[error("version conflict: expected: {expected}, got: {requested}")]
973 VersionConflict {
974 expected: Version,
975 requested: Version,
976 },
977}
978
979#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
981pub enum DbErrorWrite {
982 #[error("cannot write - row not found")]
983 NotFound,
984 #[error("non-retriable error: {0}")]
985 NonRetriable(#[from] DbErrorWriteNonRetriable),
986 #[error(transparent)]
987 Generic(#[from] DbErrorGeneric),
988}
989
990#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
992pub enum DbErrorStubResponse {
993 #[error("stub conflict: already finished with a different value")]
994 StubConflict,
995 #[error(transparent)]
996 Write(#[from] DbErrorWrite),
997}
998
999#[derive(Debug, Clone, thiserror::Error, PartialEq)]
1001pub enum DbErrorRead {
1002 #[error("cannot read - row not found")]
1003 NotFound,
1004 #[error(transparent)]
1005 Generic(#[from] DbErrorGeneric),
1006}
1007
1008#[derive(Debug, thiserror::Error, PartialEq)]
1009pub enum DbErrorReadWithTimeout {
1010 #[error("timeout")]
1011 Timeout(TimeoutOutcome),
1012 #[error(transparent)]
1013 DbErrorRead(#[from] DbErrorRead),
1014}
1015
1016#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1017pub enum ResponseSubscriptionEnd {
1018 PollIntervalElapsed,
1019 LockDeadlineReached,
1020 ExecutorClosing,
1021 ExecutionUpdated,
1022}
1023
1024#[derive(Debug, thiserror::Error, PartialEq)]
1025pub enum SubscribeToResponsesError {
1026 #[error("response subscription ended: {0:?}")]
1027 SubscriptionEnded(ResponseSubscriptionEnd),
1028 #[error(transparent)]
1029 DbErrorRead(#[from] DbErrorRead),
1030}
1031
1032pub type AppendResponse = Version;
1035pub type PendingExecution = (ExecutionId, Version, Params, Option<DateTime<Utc>>);
1036
1037#[derive(Debug, Clone)]
1038pub struct LockedExecution {
1039 pub execution_id: ExecutionId,
1040 pub next_version: Version,
1041 pub metadata: ExecutionMetadata,
1042 pub component_digest: ComponentDigest,
1043 pub locked_event: Locked,
1044 pub ffqn: FunctionFqn,
1045 pub params: Params,
1046 pub event_history: Vec<(HistoryEvent, Version)>,
1047 pub responses: Vec<ResponseWithCursor>,
1048 pub parent: Option<(ExecutionId, JoinSetId)>,
1049 pub intermittent_event_count: u32,
1050}
1051
1052pub type LockPendingResponse = Vec<LockedExecution>;
1053pub type AppendBatchResponse = Version;
1054
1055#[derive(Debug, Clone, PartialEq, derive_more::Display, Serialize, Deserialize)]
1056#[display("{event}")]
1057pub struct AppendRequest {
1058 pub created_at: DateTime<Utc>,
1059 pub event: ExecutionRequest,
1060}
1061
1062#[derive(Debug, Clone, PartialEq)]
1063#[cfg_attr(feature = "test", derive(Serialize))]
1064pub struct CreateRequest {
1065 pub created_at: DateTime<Utc>,
1066 pub execution_id: ExecutionId,
1067 pub ffqn: FunctionFqn,
1068 pub params: Params,
1069 pub parent: Option<(ExecutionId, JoinSetId)>,
1070 pub scheduled_at: DateTime<Utc>,
1071 pub component_id: ComponentId,
1072 pub deployment_id: DeploymentId,
1073 pub metadata: ExecutionMetadata,
1074 pub scheduled_by: Option<ExecutionId>,
1075 pub paused: bool,
1076}
1077
1078impl From<CreateRequest> for ExecutionRequest {
1079 fn from(value: CreateRequest) -> Self {
1080 Self::Created {
1081 ffqn: value.ffqn,
1082 params: value.params,
1083 parent: value.parent,
1084 scheduled_at: value.scheduled_at,
1085 component_id: value.component_id,
1086 deployment_id: value.deployment_id,
1087 metadata: value.metadata,
1088 scheduled_by: value.scheduled_by,
1089 }
1090 }
1091}
1092
1093#[async_trait]
1094pub trait DbPool: Send + Sync {
1095 async fn db_exec_conn(&self) -> Result<Box<dyn DbExecutor>, DbErrorGeneric>;
1096
1097 async fn connection(&self) -> Result<Box<dyn DbConnection>, DbErrorGeneric>;
1098
1099 async fn external_api_conn(&self) -> Result<Box<dyn DbExternalApi>, DbErrorGeneric>;
1100
1101 async fn cas_conn(&self) -> Result<Box<dyn crate::cas::Cas>, DbErrorGeneric>;
1105
1106 #[cfg(feature = "test")]
1107 async fn connection_test(&self) -> Result<Box<dyn DbConnectionTest>, DbErrorGeneric>;
1108}
1109
1110#[async_trait]
1111pub trait DbPoolCloseable {
1112 async fn close(&self);
1113}
1114
1115#[derive(Clone, Debug, PartialEq)]
1116#[cfg_attr(feature = "test", derive(Serialize))]
1117pub struct AppendEventsToExecution {
1118 pub execution_id: ExecutionId,
1119 pub version: Version,
1120 pub batch: Vec<AppendRequest>,
1121}
1122
1123#[derive(Clone, Debug, PartialEq)]
1124#[cfg_attr(feature = "test", derive(Serialize))]
1125pub struct AppendResponseToExecution {
1126 pub parent_execution_id: ExecutionId,
1127 pub created_at: DateTime<Utc>,
1128 pub join_set_id: JoinSetId,
1129 pub child_execution_id: ExecutionIdDerived,
1130 pub finished_version: Version,
1131 pub result: SupportedFunctionReturnValue,
1132}
1133
1134#[derive(Debug, Clone, PartialEq)]
1138#[cfg_attr(feature = "test", derive(Serialize))]
1139pub enum CapturedDbWrite {
1140 Append {
1141 execution_id: ExecutionId,
1142 version: Version,
1143 req: AppendRequest,
1144 backtraces: Vec<BacktraceInfo>,
1145 },
1146 AppendBatch {
1147 current_time: DateTime<Utc>,
1148 batch: Vec<AppendRequest>,
1149 execution_id: ExecutionId,
1150 version: Version,
1151 backtraces: Vec<BacktraceInfo>,
1152 },
1153 AppendBatchWithDelayResponse {
1154 current_time: DateTime<Utc>,
1155 batch: Vec<AppendRequest>,
1156 execution_id: ExecutionId,
1157 version: Version,
1158 join_set_id: JoinSetId,
1159 delay_id: DelayId,
1160 backtraces: Vec<BacktraceInfo>,
1161 },
1162 AppendBatchCreateNewExecution {
1163 current_time: DateTime<Utc>,
1164 batch: Vec<AppendRequest>,
1165 execution_id: ExecutionId,
1166 version: Version,
1167 child_req: Vec<CreateRequest>,
1168 backtraces: Vec<BacktraceInfo>,
1169 },
1170 AppendStubResponse {
1171 events: AppendEventsToExecution,
1172 response: AppendResponseToExecution,
1173 current_time: DateTime<Utc>,
1174 backtraces: Vec<BacktraceInfo>,
1175 },
1176 AppendFinished {
1177 execution_id: ExecutionId,
1178 version: Version,
1179 current_time: DateTime<Utc>,
1180 retval: SupportedFunctionReturnValue,
1181 parent: Option<(ExecutionId, JoinSetId)>,
1182 },
1183}
1184impl CapturedDbWrite {
1185 #[must_use]
1186 pub fn is_finished(&self) -> bool {
1187 matches!(self, CapturedDbWrite::AppendFinished { .. })
1188 }
1189}
1190
1191#[async_trait]
1192pub trait DbExecutor: Send + Sync {
1193 #[expect(clippy::too_many_arguments)]
1194 async fn lock_pending_by_ffqns(
1195 &self,
1196 batch_size: u32,
1197 pending_at_or_sooner: DateTime<Utc>,
1198 ffqns: Arc<[FunctionFqn]>,
1199 created_at: DateTime<Utc>,
1200 component_id: ComponentId,
1201 deployment_id: DeploymentId,
1202 executor_id: ExecutorId,
1203 lock_expires_at: DateTime<Utc>,
1204 run_id: RunId,
1205 retry_config: ComponentRetryConfig,
1206 ) -> Result<LockPendingResponse, DbErrorWrite>;
1207
1208 #[expect(clippy::too_many_arguments)]
1209 async fn lock_pending_by_ffqns_auto(
1210 &self,
1211 batch_size: u32,
1212 pending_at_or_sooner: DateTime<Utc>,
1213 ffqns: Arc<[FunctionFqn]>,
1214 created_at: DateTime<Utc>,
1215 component_id: ComponentId,
1216 deployment_id: DeploymentId,
1217 executor_id: ExecutorId,
1218 lock_expires_at: DateTime<Utc>,
1219 run_id: RunId,
1220 retry_config: ComponentRetryConfig,
1221 ) -> Result<LockPendingResponse, DbErrorWrite>;
1222
1223 #[expect(clippy::too_many_arguments)]
1224 async fn lock_pending_by_component_digest(
1225 &self,
1226 batch_size: u32,
1227 pending_at_or_sooner: DateTime<Utc>,
1228 component_id: &ComponentId,
1229 deployment_id: DeploymentId,
1230 created_at: DateTime<Utc>,
1231 executor_id: ExecutorId,
1232 lock_expires_at: DateTime<Utc>,
1233 run_id: RunId,
1234 retry_config: ComponentRetryConfig,
1235 ) -> Result<LockPendingResponse, DbErrorWrite>;
1236
1237 #[cfg(feature = "test")]
1238 #[expect(clippy::too_many_arguments)]
1239 async fn lock_one(
1240 &self,
1241 created_at: DateTime<Utc>,
1242 component_id: ComponentId,
1243 deployment_id: DeploymentId,
1244 execution_id: &ExecutionId,
1245 run_id: RunId,
1246 version: Version,
1247 executor_id: ExecutorId,
1248 lock_expires_at: DateTime<Utc>,
1249 retry_config: ComponentRetryConfig,
1250 ) -> Result<LockedExecution, DbErrorWrite>;
1251
1252 async fn append(
1255 &self,
1256 execution_id: ExecutionId,
1257 version: Version,
1258 req: AppendRequest,
1259 ) -> Result<AppendResponse, DbErrorWrite>;
1260
1261 async fn append_batch_respond_to_parent(
1264 &self,
1265 events: AppendEventsToExecution,
1266 response: AppendResponseToExecution,
1267 current_time: DateTime<Utc>, ) -> Result<AppendBatchResponse, DbErrorWrite>;
1269
1270 async fn wait_for_pending_by_ffqn(
1276 &self,
1277 pending_at_or_sooner: DateTime<Utc>,
1278 ffqns: Arc<[FunctionFqn]>,
1279 current_digest: Option<ComponentDigest>,
1280 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
1281 );
1282
1283 async fn wait_for_pending_by_component_digest(
1288 &self,
1289 pending_at_or_sooner: DateTime<Utc>,
1290 component_digest: &ComponentDigest,
1291 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
1292 );
1293
1294 async fn cancel_activity_with_retries(
1296 &self,
1297 execution_id: &ExecutionId,
1298 cancelled_at: DateTime<Utc>,
1299 ) -> Result<CancelOutcome, DbErrorWrite> {
1300 let mut retries = 5;
1301 loop {
1302 match self
1303 .append_activity_cancellation_requested(execution_id, cancelled_at)
1304 .await
1305 {
1306 Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::VersionConflict {
1307 ..
1308 })) if retries > 0 => retries -= 1,
1309 res => return res,
1310 }
1311 }
1312 }
1313
1314 async fn cancel_workflow(
1319 &self,
1320 execution_id: &ExecutionId,
1321 cancelled_at: DateTime<Utc>,
1322 ) -> Result<CancelOutcome, DbErrorWrite>;
1323
1324 async fn cancel_workflow_with_retries(
1327 &self,
1328 execution_id: &ExecutionId,
1329 cancelled_at: DateTime<Utc>,
1330 ) -> Result<CancelOutcome, DbErrorWrite> {
1331 let mut retries = 5;
1332 loop {
1333 match self.cancel_workflow(execution_id, cancelled_at).await {
1334 Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::VersionConflict {
1335 ..
1336 })) if retries > 0 => retries -= 1,
1337 res => return res,
1338 }
1339 }
1340 }
1341
1342 async fn get_last_execution_event(
1344 &self,
1345 execution_id: &ExecutionId,
1346 ) -> Result<ExecutionEvent, DbErrorRead>;
1347
1348 async fn append_activity_cancellation_requested(
1352 &self,
1353 execution_id: &ExecutionId,
1354 cancelled_at: DateTime<Utc>,
1355 ) -> Result<CancelOutcome, DbErrorWrite>;
1356}
1357
1358pub enum AppendDelayResponseOutcome {
1359 Success,
1360 AlreadyFinished,
1361 AlreadyCancelled,
1362}
1363
1364#[derive(Debug, Clone, Default)]
1365pub struct ListExecutionsFilter {
1366 pub function_name_filter: Option<FunctionNameFilter>,
1367 pub show_derived: bool,
1368 pub hide_finished: bool,
1369 pub execution_id_prefix: Option<String>,
1370 pub component_digest: Option<ComponentDigest>,
1371 pub deployment_id: Option<DeploymentId>,
1372 pub state_filters: Vec<ExecutionStateFilter>,
1376}
1377
1378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1380pub enum ExecutionStateFilter {
1381 Locked,
1382 Pending {
1384 now: DateTime<Utc>,
1385 },
1386 Scheduled {
1388 now: DateTime<Utc>,
1389 },
1390 Blocked,
1391 Paused,
1393 Cancelling,
1395 Finished,
1397 FinishedOk,
1399 FinishedError,
1401 FinishedExecutionFailure,
1403}
1404
1405#[derive(Debug, Clone, PartialEq, Eq)]
1406pub enum FunctionNameFilter {
1407 PackageName(String),
1408 InterfaceName(String),
1409 FunctionName(String),
1410}
1411
1412impl FunctionNameFilter {
1413 #[must_use]
1414 pub fn like_pattern(&self) -> String {
1415 match self {
1416 Self::FunctionName(function_name) | Self::InterfaceName(function_name) => {
1417 format!("{function_name}%")
1418 }
1419 Self::PackageName(package_name) => {
1420 if let Some((pkg_fqn_without_version, version)) = package_name.rsplit_once('@')
1421 && !version.is_empty()
1422 && pkg_fqn_without_version.contains(':')
1423 {
1424 format!("{pkg_fqn_without_version}/%@{version}.%")
1425 } else {
1426 format!("{package_name}%")
1427 }
1428 }
1429 }
1430 }
1431}
1432
1433#[async_trait]
1434pub trait DbExternalApi: DbConnection {
1435 async fn get_backtrace(
1437 &self,
1438 execution_id: &ExecutionId,
1439 filter: BacktraceFilter,
1440 ) -> Result<BacktraceInfo, DbErrorRead>;
1441
1442 async fn upsert_source_mapping(
1448 &self,
1449 component_digest: &ComponentDigest,
1450 frame_key: &str,
1451 is_suffix: bool,
1452 digest: &ContentDigest,
1453 ) -> Result<(), DbErrorWrite>;
1454
1455 async fn resolve_source_digest(
1460 &self,
1461 component_digest: &ComponentDigest,
1462 file: &str,
1463 ) -> Result<Option<ContentDigest>, DbErrorRead>;
1464
1465 async fn upsert_component_metadata(
1467 &self,
1468 records: Vec<ComponentMetadataRecord>,
1469 ) -> Result<(), DbErrorWrite>;
1470
1471 async fn insert_deployment_components(
1473 &self,
1474 deployment_id: DeploymentId,
1475 records: Vec<DeploymentComponentRecord>,
1476 ) -> Result<(), DbErrorWrite>;
1477
1478 async fn list_deployment_components(
1480 &self,
1481 deployment_id: DeploymentId,
1482 ) -> Result<Vec<DeploymentComponentDetail>, DbErrorRead>;
1483
1484 async fn get_deployment_component_wit(
1486 &self,
1487 deployment_id: DeploymentId,
1488 component_digest: &ComponentDigest,
1489 ) -> Result<Option<String>, DbErrorRead>;
1490
1491 async fn list_executions(
1493 &self,
1494 filter: ListExecutionsFilter,
1495 pagination: ExecutionListPagination,
1496 ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric>;
1497
1498 async fn list_execution_events(
1503 &self,
1504 execution_id: &ExecutionId,
1505 pagination: Pagination<VersionType>,
1506 include_backtrace_id: bool,
1507 ) -> Result<ListExecutionEventsResponse, DbErrorRead>;
1508
1509 async fn list_responses(
1518 &self,
1519 execution_id: &ExecutionId,
1520 pagination: Pagination<u32>,
1521 ) -> Result<ListResponsesResponse, DbErrorRead> {
1522 self.list_responses_filtered(execution_id, pagination, None)
1523 .await
1524 }
1525
1526 async fn list_responses_filtered(
1527 &self,
1528 execution_id: &ExecutionId,
1529 pagination: Pagination<u32>,
1530 join_set: Option<&JoinSetId>,
1531 ) -> Result<ListResponsesResponse, DbErrorRead>;
1532
1533 async fn list_execution_events_responses(
1534 &self,
1535 execution_id: &ExecutionId,
1536 req_since: &Version,
1537 req_max_length: VersionType,
1538 req_include_backtrace_id: bool,
1539 resp_pagination: Pagination<VersionType>,
1540 ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead>;
1541
1542 async fn upgrade_execution_component(
1543 &self,
1544 execution_id: &ExecutionId,
1545 old: &ComponentDigest,
1546 new: &ComponentDigest,
1547 reason: ComponentUpgradeReason,
1548 ) -> Result<(), DbErrorWrite>;
1549
1550 async fn list_logs(
1551 &self,
1552 execution_id: &ExecutionId,
1553 show_derived: bool,
1554 filter: LogFilter,
1555 pagination: Pagination<LogCursor>,
1556 ) -> Result<ListLogsResponse, DbErrorRead>;
1557
1558 async fn list_deployment_states(
1559 &self,
1560 current_time: DateTime<Utc>,
1561 pagination: Pagination<Option<DeploymentId>>,
1562 include_deployment_toml: bool,
1563 execution_counts: DeploymentExecutionCounts,
1564 ) -> Result<Vec<DeploymentState>, DbErrorRead>;
1565
1566 async fn insert_deployment_with_components(
1571 &self,
1572 record: DeploymentRecord,
1573 component_metadata: Vec<ComponentMetadataRecord>,
1574 deployment_components: Vec<DeploymentComponentRecord>,
1575 deployment_component_files: Vec<DeploymentComponentFileRecord>,
1576 ) -> Result<(), DbErrorWrite>;
1577
1578 async fn missing_digests(
1583 &self,
1584 deployment_id: DeploymentId,
1585 ) -> Result<Vec<ContentDigest>, DbErrorRead>;
1586
1587 async fn list_deployment_files(
1589 &self,
1590 deployment_id: DeploymentId,
1591 ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead>;
1592
1593 async fn gc_orphan_files(&self) -> Result<u64, DbErrorWrite>;
1597
1598 async fn activate_deployment(
1599 &self,
1600 deployment_id: DeploymentId,
1601 now: DateTime<Utc>,
1602 ) -> Result<(), DbErrorWrite>;
1603
1604 async fn enqueue_deployment(
1609 &self,
1610 deployment_id: DeploymentId,
1611 ) -> Result<EnqueueOutcome, DbErrorWrite>;
1612
1613 async fn get_deployment(
1615 &self,
1616 deployment_id: DeploymentId,
1617 ) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1618
1619 #[cfg(feature = "test")]
1622 async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1623
1624 async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1627
1628 async fn list_deployments(
1629 &self,
1630 pagination: Pagination<Option<DeploymentId>>,
1631 ) -> Result<Vec<DeploymentRecord>, DbErrorRead>;
1632
1633 async fn pause_execution(
1638 &self,
1639 execution_id: &ExecutionId,
1640 paused_at: DateTime<Utc>,
1641 ) -> Result<AppendResponse, DbErrorWrite>;
1642
1643 async fn unpause_execution(
1645 &self,
1646 execution_id: &ExecutionId,
1647 unpaused_at: DateTime<Utc>,
1648 ) -> Result<AppendResponse, DbErrorWrite>;
1649
1650 async fn pause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite>;
1654
1655 async fn unpause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite>;
1659}
1660pub const LIST_DEPLOYMENT_STATES_DEFAULT_LENGTH: u16 = 20;
1661pub const LIST_DEPLOYMENT_STATES_DEFAULT_PAGINATION: Pagination<Option<DeploymentId>> =
1662 Pagination::OlderThan {
1663 length: LIST_DEPLOYMENT_STATES_DEFAULT_LENGTH,
1664 cursor: None,
1665 including_cursor: false,
1666 };
1667
1668pub struct DeploymentState {
1669 pub deployment_id: DeploymentId,
1670 pub description: Option<String>,
1671 pub digest: ContentDigest,
1673 pub locked: u32,
1674 pub pending: u32,
1676 pub scheduled: u32,
1678 pub blocked: u32,
1679 pub paused: u32,
1681 pub cancelling: u32,
1683 pub finished_ok: u32,
1684 pub finished_error: u32,
1685 pub finished_execution_failure: u32,
1686 pub deployment_toml: Option<String>,
1688 pub created_at: DateTime<Utc>,
1689 pub last_active_at: Option<DateTime<Utc>>,
1691 pub status: DeploymentStatus,
1692}
1693
1694#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1695pub enum DeploymentExecutionCounts {
1696 Skip,
1698 Count { include_derived: bool },
1700}
1701
1702#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1703pub enum DeploymentStatus {
1704 Inactive,
1705 Enqueued,
1707 Active,
1708}
1709
1710#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1712pub enum EnqueueOutcome {
1713 Enqueued,
1715 AlreadyActive,
1718}
1719
1720impl DeploymentStatus {
1721 #[must_use]
1722 pub fn as_str(&self) -> &'static str {
1723 match self {
1724 DeploymentStatus::Inactive => "inactive",
1725 DeploymentStatus::Enqueued => "enqueued",
1726 DeploymentStatus::Active => "active",
1727 }
1728 }
1729}
1730
1731impl std::str::FromStr for DeploymentStatus {
1732 type Err = StrVariant;
1733 fn from_str(s: &str) -> Result<Self, Self::Err> {
1734 match s {
1735 "inactive" => Ok(DeploymentStatus::Inactive),
1736 "enqueued" => Ok(DeploymentStatus::Enqueued),
1737 "active" => Ok(DeploymentStatus::Active),
1738 _ => Err(StrVariant::from(format!("unknown deployment status: {s}"))),
1739 }
1740 }
1741}
1742
1743#[derive(Debug, Clone)]
1744pub struct DeploymentRecord {
1745 pub deployment_id: DeploymentId,
1746 pub description: Option<String>,
1747 pub digest: ContentDigest,
1749 pub created_at: DateTime<Utc>,
1750 pub last_active_at: Option<DateTime<Utc>>,
1752 pub status: DeploymentStatus,
1753 pub deployment_toml: String, pub obelisk_version: String,
1755 pub created_by: Option<String>,
1756 pub files: Vec<DeploymentFileRecord>,
1757}
1758
1759impl DeploymentRecord {
1760 #[must_use]
1762 pub fn compute_digest(deployment_toml: &str) -> ContentDigest {
1763 use sha2::{Digest as _, Sha256};
1764 let hash: [u8; 32] = Sha256::digest(deployment_toml.as_bytes()).into();
1765 ContentDigest(crate::component_id::Digest(hash))
1766 }
1767}
1768
1769#[derive(Debug, Clone, PartialEq, Eq)]
1770pub struct DeploymentFileRecord {
1771 pub path: String,
1772 pub digest: ContentDigest,
1773 pub size: u64,
1774}
1775
1776#[derive(
1777 Debug,
1778 Clone,
1779 Copy,
1780 PartialEq,
1781 Eq,
1782 serde::Serialize,
1783 serde::Deserialize,
1784 strum::Display,
1785 strum::EnumString,
1786)]
1787#[serde(rename_all = "snake_case")]
1788#[strum(serialize_all = "snake_case")]
1789pub enum ComponentFileRole {
1790 WasmComponent,
1791 ExecProgram,
1792 JsEntrypoint,
1793 JsModule,
1794 BacktraceSource,
1795 WitSource,
1796}
1797
1798#[derive(Debug, Clone, PartialEq, Eq)]
1799pub struct DeploymentComponentFileRecord {
1800 pub component_name: StrVariant,
1801 pub path: String,
1802 pub role: ComponentFileRole,
1803}
1804
1805#[derive(Debug, Clone, PartialEq, Eq)]
1806pub struct DeploymentComponentFileDetail {
1807 pub file: DeploymentFileRecord,
1808 pub role: ComponentFileRole,
1809}
1810
1811#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display, derive_more::TryFrom)]
1813#[try_from(repr)]
1814#[repr(i16)]
1815pub enum WitOrigin {
1816 #[display("wasm")]
1817 Wasm = 1,
1818 #[display("synthesized")]
1819 Synthesized = 2,
1820 #[display("authored")]
1821 Authored = 3,
1822}
1823
1824#[derive(Debug, Clone)]
1825pub struct ComponentMetadataRecord {
1826 pub component_digest: ComponentDigest,
1827 pub imports: Vec<PersistedFunctionMetadata>,
1828 pub exports: Vec<PersistedFunctionMetadata>,
1829 pub wit: String,
1830 pub wit_origin: WitOrigin,
1831}
1832
1833#[derive(Debug, Clone)]
1835pub struct DeploymentComponentRecord {
1836 pub deployment_id: DeploymentId,
1837 pub component_name: StrVariant,
1838 pub component_digest: ComponentDigest,
1839 pub component_type: ComponentType,
1840}
1841
1842#[derive(Debug, Clone)]
1843pub struct DeploymentComponentDetail {
1844 pub component_id: ComponentId,
1845 pub imports: Vec<PersistedFunctionMetadata>,
1846 pub exports: Vec<PersistedFunctionMetadata>,
1847 pub wit: String,
1848 pub files: Vec<DeploymentComponentFileDetail>,
1849}
1850
1851#[derive(
1852 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, schemars::JsonSchema,
1853)]
1854pub struct PersistedFunctionMetadata {
1855 pub ffqn: FunctionFqn,
1856 pub parameter_types: Vec<PersistedParameterType>,
1857 pub return_type: String,
1858 pub extension: Option<FunctionExtension>,
1859 pub submittable: bool,
1860}
1861
1862#[derive(
1863 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, schemars::JsonSchema,
1864)]
1865pub struct PersistedParameterType {
1866 pub name: String,
1867 pub wit_type: String,
1868}
1869
1870impl From<FunctionMetadata> for PersistedFunctionMetadata {
1871 fn from(value: FunctionMetadata) -> Self {
1872 PersistedFunctionMetadata {
1873 ffqn: value.ffqn,
1874 parameter_types: value
1875 .parameter_types
1876 .0
1877 .into_iter()
1878 .map(|param| PersistedParameterType {
1879 name: param.name.to_string(),
1880 wit_type: param.wit_type.to_string(),
1881 })
1882 .collect(),
1883 return_type: value.return_type.wit_type().to_string(),
1884 extension: value.extension,
1885 submittable: value.submittable,
1886 }
1887 }
1888}
1889
1890#[derive(Debug)]
1891pub struct ListLogsResponse {
1892 pub items: Vec<LogEntryRow>,
1893 pub next_page: Pagination<LogCursor>, pub prev_page: Option<Pagination<LogCursor>>, }
1896
1897#[derive(Debug)]
1898pub struct LogFilter {
1899 show_logs: bool,
1900 show_streams: bool,
1901 levels: Vec<LogLevel>, stream_types: Vec<LogStreamType>, created_after: Option<DateTime<Utc>>,
1904 created_before: Option<DateTime<Utc>>,
1905}
1906impl LogFilter {
1907 #[must_use]
1909 pub fn show_logs(levels: Vec<LogLevel>) -> LogFilter {
1910 LogFilter {
1911 show_logs: true,
1912 show_streams: false,
1913 levels,
1914 stream_types: Vec::new(),
1915 created_after: None,
1916 created_before: None,
1917 }
1918 }
1919 #[must_use]
1921 pub fn show_streams(stream_types: Vec<LogStreamType>) -> LogFilter {
1922 LogFilter {
1923 show_logs: false,
1924 show_streams: true,
1925 levels: Vec::new(),
1926 stream_types,
1927 created_after: None,
1928 created_before: None,
1929 }
1930 }
1931 #[must_use]
1933 pub fn show_combined(levels: Vec<LogLevel>, stream_types: Vec<LogStreamType>) -> LogFilter {
1934 LogFilter {
1935 show_logs: true,
1936 show_streams: true,
1937 levels,
1938 stream_types,
1939 created_after: None,
1940 created_before: None,
1941 }
1942 }
1943 #[must_use]
1945 pub fn should_show_logs(&self) -> bool {
1946 self.show_logs
1947 }
1948 #[must_use]
1949 pub fn should_show_streams(&self) -> bool {
1950 self.show_streams
1951 }
1952 #[must_use]
1953 pub fn levels(&self) -> &Vec<LogLevel> {
1954 &self.levels
1955 }
1956 #[must_use]
1957 pub fn stream_types(&self) -> &Vec<LogStreamType> {
1958 &self.stream_types
1959 }
1960 #[must_use]
1961 pub fn with_created_bounds(
1962 mut self,
1963 created_after: Option<DateTime<Utc>>,
1964 created_before: Option<DateTime<Utc>>,
1965 ) -> Self {
1966 self.created_after = created_after;
1967 self.created_before = created_before;
1968 self
1969 }
1970 #[must_use]
1971 pub fn created_after(&self) -> Option<DateTime<Utc>> {
1972 self.created_after
1973 }
1974 #[must_use]
1975 pub fn created_before(&self) -> Option<DateTime<Utc>> {
1976 self.created_before
1977 }
1978}
1979
1980#[derive(Debug, Clone)]
1981pub struct ExecutionWithStateRequestsResponses {
1982 pub execution_with_state: ExecutionWithState,
1983 pub events: Vec<ExecutionEvent>,
1984 pub responses: Vec<ResponseWithCursor>,
1985 pub max_version: Version,
1986 pub max_cursor: ResponseCursor,
1987}
1988
1989#[async_trait]
1990pub trait DbConnection: DbExecutor {
1991 async fn get(&self, execution_id: &ExecutionId) -> Result<ExecutionLog, DbErrorRead>;
1993
1994 async fn get_cancelling(&self, batch_size: u32) -> Result<Vec<ExecutionId>, DbErrorRead>;
1999
2000 async fn append_delay_response(
2001 &self,
2002 created_at: DateTime<Utc>,
2003 execution_id: ExecutionId,
2004 join_set_id: JoinSetId,
2005 delay_id: DelayId,
2006 outcome: Result<(), ()>, ) -> Result<AppendDelayResponseOutcome, DbErrorWrite>;
2008
2009 async fn append_batch(
2012 &self,
2013 current_time: DateTime<Utc>, batch: Vec<AppendRequest>,
2015 execution_id: ExecutionId,
2016 version: Version,
2017 ) -> Result<AppendBatchResponse, DbErrorWrite>;
2018
2019 async fn append_batch_with_delay_response(
2024 &self,
2025 current_time: DateTime<Utc>, batch: Vec<AppendRequest>,
2027 execution_id: ExecutionId,
2028 version: Version,
2029 join_set_id: JoinSetId,
2030 delay_id: DelayId,
2031 ) -> Result<AppendBatchResponse, DbErrorWrite>;
2032
2033 async fn append_batch_create_new_execution(
2036 &self,
2037 current_time: DateTime<Utc>, batch: Vec<AppendRequest>, execution_id: ExecutionId,
2040 version: Version,
2041 child_req: Vec<CreateRequest>,
2042 backtraces: Vec<BacktraceInfo>,
2043 ) -> Result<AppendBatchResponse, DbErrorWrite>;
2044
2045 async fn get_execution_event(
2047 &self,
2048 execution_id: &ExecutionId,
2049 version: &Version,
2050 ) -> Result<ExecutionEvent, DbErrorRead>;
2051
2052 async fn upsert_stub_response(
2056 &self,
2057 execution_id: ExecutionIdDerived,
2058 version: Version,
2059 req: AppendRequest,
2060 response: AppendResponseToExecution,
2061 current_time: DateTime<Utc>,
2062 ) -> Result<(), DbErrorStubResponse>;
2063
2064 #[instrument(skip(self))]
2065 async fn get_create_request(
2066 &self,
2067 execution_id: &ExecutionId,
2068 ) -> Result<CreateRequest, DbErrorRead> {
2069 let execution_event = self
2070 .get_execution_event(execution_id, &Version::new(0))
2071 .await?;
2072 if let ExecutionRequest::Created {
2073 ffqn,
2074 params,
2075 parent,
2076 scheduled_at,
2077 component_id,
2078 deployment_id,
2079 metadata,
2080 scheduled_by,
2081 } = execution_event.event
2082 {
2083 Ok(CreateRequest {
2084 created_at: execution_event.created_at,
2085 execution_id: execution_id.clone(),
2086 ffqn,
2087 params,
2088 parent,
2089 scheduled_at,
2090 component_id,
2091 deployment_id,
2092 metadata,
2093 scheduled_by,
2094 paused: false,
2095 })
2096 } else {
2097 Err(DbErrorRead::Generic(DbErrorGeneric::Uncategorized {
2098 reason: "execution log must start with creation".into(),
2099 context: SpanTrace::capture(),
2100 source: None,
2101 loc: Location::caller(),
2102 }))
2103 }
2104 }
2105
2106 async fn get_pending_state(
2107 &self,
2108 execution_id: &ExecutionId,
2109 ) -> Result<ExecutionWithState, DbErrorRead>;
2110
2111 async fn get_expired_timers(
2113 &self,
2114 at: DateTime<Utc>,
2115 ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric>;
2116
2117 async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite>;
2119
2120 async fn subscribe_to_next_responses(
2128 &self,
2129 execution_id: &ExecutionId,
2130 last_response: ResponseCursor,
2131 subscription_end_fut: Pin<Box<dyn Future<Output = ResponseSubscriptionEnd> + Send>>,
2132 ) -> Result<Vec<ResponseWithCursor>, SubscribeToResponsesError>;
2133
2134 async fn wait_for_finished_result(
2141 &self,
2142 execution_id: &ExecutionId,
2143 timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
2144 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout>;
2145
2146 async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite>;
2147
2148 async fn append_backtrace_batch(
2149 &self,
2150 batch: Vec<BacktraceInfo>,
2151 ) -> Result<usize, DbErrorWrite>;
2152
2153 async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite>;
2154
2155 async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite>;
2156
2157 #[cfg(feature = "test")]
2159 async fn get_finished_result(
2160 &self,
2161 execution_id: &ExecutionId,
2162 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
2163 self.wait_for_finished_result(
2164 execution_id,
2165 Some(Box::pin(std::future::ready(TimeoutOutcome::Timeout))),
2166 )
2167 .await
2168 }
2169}
2170
2171#[derive(Clone, Debug)]
2172pub struct LogInfoAppendRow {
2173 pub execution_id: ExecutionId,
2174 pub run_id: RunId,
2175 pub log_entry: LogEntry,
2176}
2177
2178#[derive(Debug, Clone)]
2179pub struct LogEntryRow {
2180 pub cursor: LogCursor,
2181 pub run_id: RunId,
2182 pub log_entry: LogEntry,
2183 pub execution_id: ExecutionId,
2184}
2185
2186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2187pub struct LogCursor(pub i64);
2188
2189#[derive(Debug, Clone)]
2190pub enum LogEntry {
2191 Log {
2192 created_at: DateTime<Utc>,
2193 level: LogLevel,
2194 message: String,
2195 },
2196 Stream {
2197 created_at: DateTime<Utc>,
2198 payload: Vec<u8>,
2199 stream_type: LogStreamType,
2200 },
2201}
2202impl LogEntry {
2203 #[must_use]
2204 pub fn created_at(&self) -> DateTime<Utc> {
2205 match self {
2206 LogEntry::Log { created_at, .. } | LogEntry::Stream { created_at, .. } => *created_at,
2207 }
2208 }
2209}
2210
2211#[derive(
2212 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, derive_more::TryFrom, strum::EnumIter,
2213)]
2214#[try_from(repr)]
2215#[repr(u8)]
2216pub enum LogLevel {
2217 Trace = 1,
2218 Debug,
2219 Info,
2220 Warn,
2221 Error,
2222}
2223#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::TryFrom, strum::EnumIter)]
2224#[try_from(repr)]
2225#[repr(u8)]
2226pub enum LogStreamType {
2227 StdOut = 1,
2228 StdErr,
2229}
2230
2231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2232pub enum TimeoutOutcome {
2233 Timeout,
2234 Cancel,
2235}
2236
2237#[cfg(feature = "test")]
2238#[async_trait]
2239pub trait DbConnectionTest: DbConnection {
2240 async fn append_response(
2241 &self,
2242 created_at: DateTime<Utc>,
2243 execution_id: ExecutionId,
2244 response_event: JoinSetResponseEvent,
2245 ) -> Result<(), DbErrorWrite>;
2246}
2247
2248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2249pub enum CancelOutcome {
2250 CancelRequested,
2251 AlreadyFinished,
2252 AlreadyCancelling,
2253}
2254
2255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2256pub enum DelayCancelOutcome {
2257 Cancelled,
2258 AlreadyFinished,
2259}
2260
2261#[instrument(skip(db_connection))]
2262pub async fn stub_execution(
2263 db_connection: &dyn DbConnection,
2264 execution_id: ExecutionIdDerived,
2265 parent_execution_id: ExecutionId,
2266 join_set_id: JoinSetId,
2267 created_at: DateTime<Utc>,
2268 return_value: SupportedFunctionReturnValue,
2269) -> Result<(), DbErrorWrite> {
2270 let stub_finished_version = Version::new(1); let finished_req = AppendRequest {
2272 created_at,
2273 event: ExecutionRequest::Finished {
2274 retval: return_value.clone(),
2275 http_client_traces: None,
2276 },
2277 };
2278 db_connection
2279 .upsert_stub_response(
2280 execution_id.clone(),
2281 stub_finished_version.clone(),
2282 finished_req,
2283 AppendResponseToExecution {
2284 parent_execution_id,
2285 created_at,
2286 join_set_id,
2287 child_execution_id: execution_id,
2288 finished_version: stub_finished_version,
2289 result: return_value,
2290 },
2291 created_at,
2292 )
2293 .await
2294 .map_err(|err| match err {
2295 DbErrorStubResponse::StubConflict => {
2296 DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)
2297 }
2298 DbErrorStubResponse::Write(db_err) => db_err,
2299 })
2300}
2301
2302pub async fn cancel_delay(
2303 db_connection: &dyn DbConnection,
2304 delay_id: DelayId,
2305 cancelled_at: DateTime<Utc>,
2306) -> Result<DelayCancelOutcome, DbErrorWrite> {
2307 let (parent_execution_id, join_set_id) = delay_id.split_to_parts();
2308 db_connection
2309 .append_delay_response(
2310 cancelled_at,
2311 parent_execution_id,
2312 join_set_id,
2313 delay_id,
2314 Err(()), )
2316 .await
2317 .map(|ok| match ok {
2318 AppendDelayResponseOutcome::Success | AppendDelayResponseOutcome::AlreadyCancelled => {
2319 DelayCancelOutcome::Cancelled
2320 }
2321 AppendDelayResponseOutcome::AlreadyFinished => DelayCancelOutcome::AlreadyFinished,
2322 })
2323}
2324
2325#[derive(Clone, Debug)]
2326pub enum BacktraceFilter {
2327 First,
2328 Last,
2329 Specific(Version),
2330}
2331
2332#[derive(Clone, Debug, PartialEq, Eq)]
2333#[cfg_attr(feature = "test", derive(Serialize))]
2334pub struct BacktraceInfo {
2335 pub execution_id: ExecutionId,
2336 pub component_id: ComponentId,
2337 pub version_min_including: Version,
2338 pub version_max_excluding: Version,
2339 pub wasm_backtrace: WasmBacktrace,
2340}
2341
2342#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2343pub struct WasmBacktrace {
2344 pub frames: Vec<FrameInfo>,
2345}
2346
2347#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2348pub struct FrameInfo {
2349 pub module: String,
2350 pub func_name: String,
2351 pub symbols: Vec<FrameSymbol>,
2352}
2353
2354#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2355pub struct FrameSymbol {
2356 pub func_name: Option<String>,
2357 pub file: Option<String>,
2358 pub line: Option<u32>,
2359 pub col: Option<u32>,
2360}
2361
2362mod wasm_backtrace {
2363 use super::{FrameInfo, FrameSymbol, WasmBacktrace};
2364
2365 impl WasmBacktrace {
2366 pub fn maybe_from(backtrace: &wasmtime::WasmBacktrace) -> Option<Self> {
2367 if backtrace.frames().is_empty() {
2368 None
2369 } else {
2370 Some(Self {
2371 frames: backtrace.frames().iter().map(FrameInfo::from).collect(),
2372 })
2373 }
2374 }
2375 }
2376
2377 impl From<&wasmtime::FrameInfo> for FrameInfo {
2378 fn from(frame: &wasmtime::FrameInfo) -> Self {
2379 let module_name = frame.module().name().unwrap_or("<unknown>").to_string();
2380 let mut func_name = String::new();
2381 wasmtime_environ::demangle_function_name_or_index(
2382 &mut func_name,
2383 frame.func_name(),
2384 frame.func_index() as usize,
2385 )
2386 .expect("writing to string must succeed");
2387 Self {
2388 module: module_name,
2389 func_name,
2390 symbols: frame
2391 .symbols()
2392 .iter()
2393 .map(std::convert::Into::into)
2394 .collect(),
2395 }
2396 }
2397 }
2398
2399 impl From<&wasmtime::FrameSymbol> for FrameSymbol {
2400 fn from(symbol: &wasmtime::FrameSymbol) -> Self {
2401 let func_name = symbol.name().map(|name| {
2402 let mut writer = String::new();
2403 wasmtime_environ::demangle_function_name(&mut writer, name)
2404 .expect("writing to string must succeed");
2405 writer
2406 });
2407
2408 Self {
2409 func_name,
2410 file: symbol.file().map(ToString::to_string),
2411 line: symbol.line(),
2412 col: symbol.column(),
2413 }
2414 }
2415 }
2416}
2417#[derive(Debug, Clone, derive_more::Display)]
2418#[display("{execution_id} {pending_state} {component_digest}")]
2419pub struct ExecutionWithState {
2420 pub execution_id: ExecutionId,
2421 pub ffqn: FunctionFqn,
2422 pub pending_state: PendingState,
2423 pub created_at: DateTime<Utc>,
2424 pub first_scheduled_at: DateTime<Utc>,
2425 pub component_digest: ComponentDigest,
2426 pub component_type: ComponentType,
2427 pub deployment_id: DeploymentId,
2428}
2429
2430#[derive(Debug, Clone)]
2431pub enum ExecutionListPagination {
2432 CreatedBy(Pagination<Option<DateTime<Utc>>>),
2433 ExecutionId(Pagination<Option<ExecutionId>>),
2434}
2435impl Default for ExecutionListPagination {
2436 fn default() -> ExecutionListPagination {
2437 ExecutionListPagination::CreatedBy(Pagination::OlderThan {
2438 length: 20,
2439 cursor: None,
2440 including_cursor: false, })
2442 }
2443}
2444impl ExecutionListPagination {
2445 #[must_use]
2446 pub fn length(&self) -> u16 {
2447 match self {
2448 ExecutionListPagination::CreatedBy(pagination) => pagination.length(),
2449 ExecutionListPagination::ExecutionId(pagination) => pagination.length(),
2450 }
2451 }
2452}
2453
2454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2455pub enum Pagination<T> {
2456 NewerThan {
2457 length: u16,
2458 cursor: T,
2459 including_cursor: bool,
2460 },
2461 OlderThan {
2462 length: u16,
2463 cursor: T,
2464 including_cursor: bool,
2465 },
2466}
2467impl<T: Clone> Pagination<T> {
2468 pub fn length(&self) -> u16 {
2469 match self {
2470 Pagination::NewerThan { length, .. } | Pagination::OlderThan { length, .. } => *length,
2471 }
2472 }
2473
2474 pub fn rel(&self) -> &'static str {
2475 match self {
2476 Pagination::NewerThan {
2477 including_cursor: false,
2478 ..
2479 } => ">",
2480 Pagination::NewerThan {
2481 including_cursor: true,
2482 ..
2483 } => ">=",
2484 Pagination::OlderThan {
2485 including_cursor: false,
2486 ..
2487 } => "<",
2488 Pagination::OlderThan {
2489 including_cursor: true,
2490 ..
2491 } => "<=",
2492 }
2493 }
2494
2495 pub fn is_desc(&self) -> bool {
2496 matches!(self, Pagination::OlderThan { .. })
2497 }
2498
2499 pub fn asc_or_desc(&self) -> &'static str {
2500 if self.is_asc() { "asc" } else { "desc" }
2501 }
2502
2503 pub fn is_asc(&self) -> bool {
2504 !self.is_desc()
2505 }
2506
2507 pub fn cursor(&self) -> &T {
2508 match self {
2509 Pagination::NewerThan { cursor, .. } | Pagination::OlderThan { cursor, .. } => cursor,
2510 }
2511 }
2512
2513 #[must_use]
2514 pub fn invert(&self) -> Self {
2515 match self {
2516 Pagination::NewerThan {
2517 length,
2518 cursor,
2519 including_cursor,
2520 } => Pagination::OlderThan {
2521 length: *length,
2522 cursor: cursor.clone(),
2523 including_cursor: !including_cursor,
2524 },
2525 Pagination::OlderThan {
2526 length,
2527 cursor,
2528 including_cursor,
2529 } => Pagination::NewerThan {
2530 length: *length,
2531 cursor: cursor.clone(),
2532 including_cursor: !including_cursor,
2533 },
2534 }
2535 }
2536}
2537
2538#[cfg(feature = "test")]
2539pub async fn wait_for_pending_state_fn<T: Debug>(
2540 db_connection: &dyn DbConnectionTest,
2541 execution_id: &ExecutionId,
2542 predicate: impl Fn(ExecutionLog) -> Option<T> + Send,
2543 timeout: Option<Duration>,
2544) -> Result<T, DbErrorReadWithTimeout> {
2545 tracing::trace!(%execution_id, "Waiting for predicate");
2546 let fut = async move {
2547 loop {
2548 let execution_log = db_connection.get(execution_id).await?;
2549 if let Some(t) = predicate(execution_log) {
2550 tracing::debug!(%execution_id, "Found: {t:?}");
2551 return Ok(t);
2552 }
2553 tokio::time::sleep(Duration::from_millis(10)).await;
2554 }
2555 };
2556
2557 if let Some(timeout) = timeout {
2558 tokio::select! { res = fut => res,
2560 () = tokio::time::sleep(timeout) => Err(DbErrorReadWithTimeout::Timeout(TimeoutOutcome::Timeout))
2561 }
2562 } else {
2563 fut.await
2564 }
2565}
2566
2567#[derive(Debug, Clone, PartialEq, Eq)]
2568pub enum ExpiredTimer {
2569 Lock(ExpiredLock),
2570 Delay(ExpiredDelay),
2571}
2572
2573#[derive(Debug, Clone, PartialEq, Eq)]
2574pub struct ExpiredLock {
2575 pub execution_id: ExecutionId,
2576 pub locked_at_version: Version,
2578 pub next_version: Version,
2579 pub intermittent_event_count: u32,
2581 pub max_retries: Option<u32>,
2582 pub retry_exp_backoff: Duration,
2583 pub locked_by: LockedBy,
2584}
2585
2586#[derive(Debug, Clone, PartialEq, Eq)]
2587pub struct ExpiredDelay {
2588 pub execution_id: ExecutionId,
2589 pub join_set_id: JoinSetId,
2590 pub delay_id: DelayId,
2591}
2592
2593#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2594#[serde(tag = "status", rename_all = "snake_case")]
2595pub enum PendingState {
2596 Locked(PendingStateLocked),
2598
2599 #[display("PendingAt(`{_0}`)")]
2600 PendingAt(PendingStatePendingAt),
2601
2602 #[display("BlockedByJoinSet({_0})")]
2604 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2605
2606 #[display("Paused({_0})")]
2614 Paused(PendingStatePaused),
2615
2616 #[display("Cancelling({_0})")]
2619 Cancelling(PendingStateCancelling),
2620
2621 #[display("Finished: {_0}")]
2622 Finished(PendingStateFinished),
2623}
2624
2625pub enum PendingStateMerged {
2628 Locked {
2629 state: PendingStateLocked,
2630 lifecycle: Lifecycle,
2631 },
2632 PendingAt {
2633 state: PendingStatePendingAt,
2634 lifecycle: Lifecycle,
2635 },
2636 BlockedByJoinSet {
2637 state: PendingStateBlockedByJoinSet,
2638 lifecycle: Lifecycle,
2639 },
2640 Finished(PendingStateFinished),
2641}
2642impl From<PendingState> for PendingStateMerged {
2643 fn from(state: PendingState) -> Self {
2644 match state {
2645 PendingState::Locked(s) => PendingStateMerged::Locked {
2646 state: s,
2647 lifecycle: Lifecycle::Active,
2648 },
2649
2650 PendingState::PendingAt(s) => PendingStateMerged::PendingAt {
2651 state: s,
2652 lifecycle: Lifecycle::Active,
2653 },
2654
2655 PendingState::BlockedByJoinSet(s) => PendingStateMerged::BlockedByJoinSet {
2656 state: s,
2657 lifecycle: Lifecycle::Active,
2658 },
2659
2660 PendingState::Paused(inner) => match inner {
2661 PendingStatePaused::PendingAt(s) => PendingStateMerged::PendingAt {
2662 state: s,
2663 lifecycle: Lifecycle::Paused,
2664 },
2665 PendingStatePaused::BlockedByJoinSet(s) => PendingStateMerged::BlockedByJoinSet {
2666 state: s,
2667 lifecycle: Lifecycle::Paused,
2668 },
2669 },
2670
2671 PendingState::Cancelling(inner) => match inner {
2672 PendingStateCancelling::Locked(s) => PendingStateMerged::Locked {
2673 state: s,
2674 lifecycle: Lifecycle::Cancelling,
2675 },
2676 PendingStateCancelling::PendingAt(s) => PendingStateMerged::PendingAt {
2677 state: s,
2678 lifecycle: Lifecycle::Cancelling,
2679 },
2680 PendingStateCancelling::BlockedByJoinSet(s) => {
2681 PendingStateMerged::BlockedByJoinSet {
2682 state: s,
2683 lifecycle: Lifecycle::Cancelling,
2684 }
2685 }
2686 },
2687
2688 PendingState::Finished(s) => PendingStateMerged::Finished(s),
2689 }
2690 }
2691}
2692
2693#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2694#[display("Locked(`{lock_expires_at}`, {}, {})", locked_by.executor_id, locked_by.run_id)]
2695pub struct PendingStateLocked {
2696 pub locked_by: LockedBy,
2697 pub lock_expires_at: DateTime<Utc>,
2698}
2699
2700#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2701#[display("`{scheduled_at}`, last_lock={last_lock:?}")]
2702pub struct PendingStatePendingAt {
2703 pub scheduled_at: DateTime<Utc>,
2704 pub last_lock: Option<LockedBy>,
2706}
2707
2708#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2709#[display("{join_set_id}, `{lock_expires_at}`, closing={closing}")]
2710pub struct PendingStateBlockedByJoinSet {
2711 pub join_set_id: JoinSetId,
2712 pub lock_expires_at: DateTime<Utc>,
2714 pub closing: bool,
2716}
2717
2718#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2723pub enum PendingStatePaused {
2724 #[display("PendingAt({_0})")]
2725 PendingAt(PendingStatePendingAt),
2726 #[display("BlockedByJoinSet({_0})")]
2727 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2728}
2729
2730#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2737pub enum PendingStateCancelling {
2738 #[display("Locked({_0})")]
2739 Locked(PendingStateLocked),
2740 #[display("PendingAt({_0})")]
2741 PendingAt(PendingStatePendingAt),
2742 #[display("BlockedByJoinSet({_0})")]
2743 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2744}
2745
2746#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2747pub struct LockedBy {
2748 pub executor_id: ExecutorId,
2749 pub run_id: RunId,
2750}
2751impl From<&Locked> for LockedBy {
2752 fn from(value: &Locked) -> Self {
2753 LockedBy {
2754 executor_id: value.executor_id,
2755 run_id: value.run_id,
2756 }
2757 }
2758}
2759
2760#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2761#[cfg_attr(any(test, feature = "test"), derive(Deserialize))]
2762pub struct PendingStateFinished {
2763 pub version: VersionType, pub finished_at: DateTime<Utc>,
2765 pub result_kind: PendingStateFinishedResultKind,
2766}
2767impl Display for PendingStateFinished {
2768 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2769 match self.result_kind {
2770 PendingStateFinishedResultKind::Ok => write!(f, "OK"),
2771 PendingStateFinishedResultKind::Err(err) => write!(f, "{err}"),
2772 }
2773 }
2774}
2775
2776#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2778#[serde(rename_all = "snake_case")]
2779pub enum PendingStateFinishedResultKind {
2780 Ok,
2781 Err(PendingStateFinishedError),
2782}
2783impl PendingStateFinishedResultKind {
2784 pub fn as_result(&self) -> Result<(), &PendingStateFinishedError> {
2785 match self {
2786 PendingStateFinishedResultKind::Ok => Ok(()),
2787 PendingStateFinishedResultKind::Err(err) => Err(err),
2788 }
2789 }
2790}
2791
2792impl From<&SupportedFunctionReturnValue> for PendingStateFinishedResultKind {
2793 fn from(result: &SupportedFunctionReturnValue) -> Self {
2794 result.as_pending_state_finished_result()
2795 }
2796}
2797
2798#[derive(
2799 Debug,
2800 Clone,
2801 Copy,
2802 PartialEq,
2803 Eq,
2804 Serialize,
2805 Deserialize,
2806 derive_more::Display,
2807 schemars::JsonSchema,
2808)]
2809#[serde(rename_all = "snake_case")]
2810pub enum PendingStateFinishedError {
2811 #[display("Execution failure ({_0})")]
2812 ExecutionFailure(ExecutionFailureKind),
2813 #[display("Error")]
2814 Error,
2815}
2816
2817impl PendingState {
2818 #[instrument(skip(self))]
2819 pub fn can_append_lock(
2820 &self,
2821 created_at: DateTime<Utc>,
2822 executor_id: ExecutorId,
2823 run_id: RunId,
2824 lock_expires_at: DateTime<Utc>,
2825 ) -> Result<LockKind, DbErrorWriteNonRetriable> {
2826 if lock_expires_at <= created_at {
2827 return Err(DbErrorWriteNonRetriable::ValidationFailed(
2828 "invalid expiry date".into(),
2829 ));
2830 }
2831 match self {
2832 PendingState::PendingAt(PendingStatePendingAt {
2833 scheduled_at,
2834 last_lock,
2835 }) => {
2836 if *scheduled_at <= created_at {
2837 Ok(LockKind::CreatingNewLock)
2839 } else if let Some(LockedBy {
2840 executor_id: last_executor_id,
2841 run_id: last_run_id,
2842 }) = last_lock
2843 && executor_id == *last_executor_id
2844 && run_id == *last_run_id
2845 {
2846 Ok(LockKind::Extending)
2848 } else {
2849 Err(DbErrorWriteNonRetriable::ValidationFailed(
2850 "cannot lock, not yet pending".into(),
2851 ))
2852 }
2853 }
2854 PendingState::Locked(PendingStateLocked {
2855 locked_by:
2856 LockedBy {
2857 executor_id: current_pending_state_executor_id,
2858 run_id: current_pending_state_run_id,
2859 },
2860 lock_expires_at: _,
2861 }) => {
2862 if executor_id == *current_pending_state_executor_id
2863 && run_id == *current_pending_state_run_id
2864 {
2865 Ok(LockKind::Extending)
2867 } else {
2868 Err(DbErrorWriteNonRetriable::IllegalState {
2869 reason: "cannot lock, already locked".into(),
2870 context: SpanTrace::capture(),
2871 source: None,
2872 loc: Location::caller(),
2873 })
2874 }
2875 }
2876 PendingState::BlockedByJoinSet { .. } => Err(DbErrorWriteNonRetriable::IllegalState {
2877 reason: "cannot append Locked event when in BlockedByJoinSet state".into(),
2878 context: SpanTrace::capture(),
2879 source: None,
2880 loc: Location::caller(),
2881 }),
2882 PendingState::Finished { .. } => Err(DbErrorWriteNonRetriable::IllegalState {
2883 reason: "already finished".into(),
2884 context: SpanTrace::capture(),
2885 source: None,
2886 loc: Location::caller(),
2887 }),
2888 PendingState::Paused(..) => Err(DbErrorWriteNonRetriable::IllegalState {
2889 reason: "cannot lock, execution is paused".into(),
2890 context: SpanTrace::capture(),
2891 source: None,
2892 loc: Location::caller(),
2893 }),
2894 PendingState::Cancelling(..) => Err(DbErrorWriteNonRetriable::IllegalState {
2895 reason: "cannot lock, execution is cancelling".into(),
2896 context: SpanTrace::capture(),
2897 source: None,
2898 loc: Location::caller(),
2899 }),
2900 }
2901 }
2902
2903 #[must_use]
2904 pub fn is_finished(&self) -> bool {
2905 matches!(self, PendingState::Finished { .. })
2906 }
2907
2908 #[must_use]
2909 pub fn is_paused(&self) -> bool {
2910 matches!(self, PendingState::Paused(_))
2911 }
2912
2913 #[must_use]
2914 pub fn is_cancelling(&self) -> bool {
2915 matches!(self, PendingState::Cancelling(_))
2916 }
2917}
2918
2919#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2920pub enum LockKind {
2921 Extending,
2922 CreatingNewLock,
2923}
2924
2925pub mod http_client_trace {
2926 use chrono::{DateTime, Utc};
2927 use serde::{Deserialize, Serialize};
2928
2929 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2930 pub struct HttpClientTrace {
2931 pub req: RequestTrace,
2932 pub resp: Option<ResponseTrace>,
2933 }
2934
2935 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2936 pub struct RequestTrace {
2937 pub sent_at: DateTime<Utc>,
2938 pub uri: String,
2939 pub method: String,
2940 }
2941
2942 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2943 pub struct ResponseTrace {
2944 pub finished_at: DateTime<Utc>,
2945 pub status: Result<u16, String>,
2946 }
2947}
2948
2949#[derive(schemars::JsonSchema)]
2951pub struct DbStorageSchema {
2952 pub execution_event: ExecutionEvent,
2953 pub pending_state: PendingState,
2954 pub join_set_response: JoinSetResponse,
2955 pub wasm_backtrace: WasmBacktrace,
2956 pub persisted_function_metadata: PersistedFunctionMetadata,
2957}
2958
2959#[cfg(test)]
2960mod tests {
2961 use super::HistoryEvent;
2962 use super::HistoryEventScheduleAt;
2963 use super::JoinNextTryOutcome;
2964 use super::PendingStateFinished;
2965 use super::PendingStateFinishedError;
2966 use super::PendingStateFinishedResultKind;
2967 use crate::ExecutionFailureKind;
2968 use crate::JoinSetId;
2969 use crate::SupportedFunctionReturnValue;
2970 use chrono::DateTime;
2971 use chrono::Datelike;
2972 use insta::assert_snapshot;
2973 use rstest::rstest;
2974 use std::time::Duration;
2975 use val_json::type_wrapper::TypeWrapper;
2976 use val_json::wast_val::WastVal;
2977 use val_json::wast_val::WastValWithType;
2978
2979 #[rstest(expected => [
2980 PendingStateFinishedResultKind::Ok,
2981 PendingStateFinishedResultKind::Err(PendingStateFinishedError::ExecutionFailure(ExecutionFailureKind::TimedOut)),
2982 ])]
2983 #[test]
2984 fn serde_pending_state_finished_result_kind_should_work(
2985 expected: PendingStateFinishedResultKind,
2986 ) {
2987 let ser = serde_json::to_string(&expected).unwrap();
2988 let actual: PendingStateFinishedResultKind = serde_json::from_str(&ser).unwrap();
2989 assert_eq!(expected, actual);
2990 }
2991
2992 #[test]
2993 fn result_kind_json_constants_match_serde() {
2994 assert_eq!(
2995 crate::storage::RESULT_KIND_JSON_OK,
2996 serde_json::to_string(&PendingStateFinishedResultKind::Ok).unwrap()
2997 );
2998 assert_eq!(
2999 crate::storage::RESULT_KIND_JSON_ERROR,
3000 serde_json::to_string(&PendingStateFinishedResultKind::Err(
3001 PendingStateFinishedError::Error
3002 ))
3003 .unwrap()
3004 );
3005 }
3006
3007 #[rstest(result_kind => [
3008 PendingStateFinishedResultKind::Ok,
3009 PendingStateFinishedResultKind::Err(PendingStateFinishedError::ExecutionFailure(ExecutionFailureKind::TimedOut)),
3010 ])]
3011 #[test]
3012 fn serde_pending_state_finished_should_work(result_kind: PendingStateFinishedResultKind) {
3013 let expected = PendingStateFinished {
3014 version: 0,
3015 finished_at: DateTime::UNIX_EPOCH,
3016 result_kind,
3017 };
3018
3019 let ser = serde_json::to_string(&expected).unwrap();
3020 let actual: PendingStateFinished = serde_json::from_str(&ser).unwrap();
3021 assert_eq!(expected, actual);
3022 }
3023
3024 #[test]
3025 fn join_set_deser_with_result_ok_option_none_should_work() {
3026 let expected = SupportedFunctionReturnValue::Ok(Some(WastValWithType {
3027 r#type: TypeWrapper::Result {
3028 ok: Some(Box::new(TypeWrapper::Option(Box::new(TypeWrapper::String)))),
3029 err: Some(Box::new(TypeWrapper::String)),
3030 },
3031 value: WastVal::Result(Ok(Some(Box::new(WastVal::Option(None))))),
3032 }));
3033 let json = serde_json::to_string(&expected).unwrap();
3034 assert_snapshot!(json);
3035
3036 let actual: SupportedFunctionReturnValue = serde_json::from_str(&json).unwrap();
3037
3038 assert_eq!(expected, actual);
3039 }
3040
3041 #[test]
3042 fn as_date_time_should_work_with_duration_u32_max_secs() {
3043 let duration = Duration::from_secs(u64::from(u32::MAX));
3044 let schedule_at = HistoryEventScheduleAt::In(duration);
3045 let resolved = schedule_at.as_date_time(DateTime::UNIX_EPOCH).unwrap();
3046 assert_eq!(2106, resolved.year());
3047 }
3048
3049 const MILLIS_PER_SEC: i64 = 1000;
3050 const TIMEDELTA_MAX_SECS: i64 = i64::MAX / MILLIS_PER_SEC;
3051
3052 #[test]
3053 fn as_date_time_should_fail_on_duration_secs_greater_than_i64_max() {
3054 let duration = Duration::from_secs(
3056 u64::try_from(TIMEDELTA_MAX_SECS).expect("positive number must not fail") + 1,
3057 );
3058 let schedule_at = HistoryEventScheduleAt::In(duration);
3059 schedule_at.as_date_time(DateTime::UNIX_EPOCH).unwrap_err();
3060 }
3061
3062 #[test]
3063 fn join_next_try_outcome_new_format() {
3064 let json = r#"{"type":"join_next_try","join_set_id":"n:test","outcome":"found"}"#;
3065 let event: HistoryEvent = serde_json::from_str(json).unwrap();
3066 assert_eq!(
3067 event,
3068 HistoryEvent::JoinNextTry {
3069 join_set_id: JoinSetId::new(
3070 crate::JoinSetKind::Named,
3071 crate::StrVariant::Static("test")
3072 )
3073 .unwrap(),
3074 outcome: JoinNextTryOutcome::Found,
3075 }
3076 );
3077
3078 let json = r#"{"type":"join_next_try","join_set_id":"n:test","outcome":"all_processed"}"#;
3079 let event: HistoryEvent = serde_json::from_str(json).unwrap();
3080 assert_eq!(
3081 event,
3082 HistoryEvent::JoinNextTry {
3083 join_set_id: JoinSetId::new(
3084 crate::JoinSetKind::Named,
3085 crate::StrVariant::Static("test")
3086 )
3087 .unwrap(),
3088 outcome: JoinNextTryOutcome::AllProcessed,
3089 }
3090 );
3091 }
3092
3093 #[test]
3094 fn join_next_try_outcome_serializes_new_format() {
3095 let event = HistoryEvent::JoinNextTry {
3096 join_set_id: JoinSetId::new(
3097 crate::JoinSetKind::Named,
3098 crate::StrVariant::Static("test"),
3099 )
3100 .unwrap(),
3101 outcome: JoinNextTryOutcome::AllProcessed,
3102 };
3103 let json = serde_json::to_string(&event).unwrap();
3104 assert!(
3105 json.contains(r#""outcome":"all_processed""#),
3106 "expected outcome field, got: {json}"
3107 );
3108 assert!(
3109 !json.contains("found_response"),
3110 "should not contain old field, got: {json}"
3111 );
3112 }
3113
3114 mod stub_retval_hash {
3115 use super::super::{StubRetVal, StubRetValHash};
3116 use crate::SupportedFunctionReturnValue;
3117 use val_json::type_wrapper::TypeWrapper;
3118 use val_json::wast_val::{WastVal, WastValWithType};
3119
3120 #[test]
3121 fn typed_variant_hash_is_stable() {
3122 let retval =
3123 StubRetVal::Typed(SupportedFunctionReturnValue::Ok(Some(WastValWithType {
3124 r#type: TypeWrapper::String,
3125 value: WastVal::String("hello".into()),
3126 })));
3127 let hash = retval.hash();
3128 assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
3130 assert_eq!(hash.to_string().len(), 66);
3132 }
3133
3134 #[test]
3135 fn untyped_variant_hash_is_stable() {
3136 let retval = StubRetVal::Untyped(r#"{"ok": "hello"}"#.to_string());
3137 let hash = retval.hash();
3138 assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
3140 assert_eq!(hash.to_string().len(), 66);
3142 }
3143
3144 #[test]
3145 fn different_values_produce_different_hashes() {
3146 let typed1 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3147 let typed2 = StubRetVal::Typed(SupportedFunctionReturnValue::Err(None));
3148 let untyped1 = StubRetVal::Untyped("value1".to_string());
3149 let untyped2 = StubRetVal::Untyped("value2".to_string());
3150
3151 let hashes: Vec<_> = [typed1, typed2, untyped1, untyped2]
3152 .into_iter()
3153 .map(|r| r.hash().to_string())
3154 .collect();
3155
3156 for (i, h1) in hashes.iter().enumerate() {
3158 for h2 in hashes.iter().skip(i + 1) {
3159 assert_ne!(h1, h2, "hashes should be different");
3160 }
3161 }
3162 }
3163
3164 #[test]
3165 fn same_values_produce_same_hashes() {
3166 let retval1 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3167 let retval2 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3168 assert_eq!(retval1.hash(), retval2.hash());
3169
3170 let untyped1 = StubRetVal::Untyped("test".to_string());
3171 let untyped2 = StubRetVal::Untyped("test".to_string());
3172 assert_eq!(untyped1.hash(), untyped2.hash());
3173 }
3174
3175 #[test]
3176 fn hash_serialization_roundtrip() {
3177 let retval = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3178 let hash = retval.hash();
3179
3180 let serialized = serde_json::to_string(&hash).unwrap();
3181 let deserialized: StubRetValHash = serde_json::from_str(&serialized).unwrap();
3182
3183 assert_eq!(hash, deserialized);
3184 }
3185
3186 #[test]
3187 fn hash_display_and_fromstr_roundtrip() {
3188 let retval = StubRetVal::Untyped("test value".to_string());
3189 let hash = retval.hash();
3190
3191 let display = hash.to_string();
3192 let parsed: StubRetValHash = display.parse().unwrap();
3193
3194 assert_eq!(hash, parsed);
3195 }
3196
3197 #[test]
3198 fn typed_and_untyped_with_same_content_produce_different_hashes() {
3199 let typed = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3201 let json_of_typed =
3202 serde_json::to_string(&SupportedFunctionReturnValue::Ok(None)).unwrap();
3203 let untyped = StubRetVal::Untyped(json_of_typed);
3204
3205 assert_ne!(typed.hash(), untyped.hash());
3206 }
3207 }
3208}