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}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize , schemars::JsonSchema)]
317pub struct JoinSetResponseEventOuter {
318 pub created_at: DateTime<Utc>,
319 pub event: JoinSetResponseEvent,
320}
321
322#[derive(
323 Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
324)]
325pub struct JoinSetResponseEvent {
326 pub join_set_id: JoinSetId,
327 pub event: JoinSetResponse,
328}
329
330#[derive(
331 Clone, Debug, PartialEq, Eq, Serialize, Deserialize, derive_more::Display, schemars::JsonSchema,
332)]
333#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
334#[serde(tag = "type", rename_all = "snake_case")]
335pub enum JoinSetResponse {
336 #[display("delay {}: {delay_id}", if result.is_ok() { "finished" } else { "cancelled"})]
337 DelayFinished {
338 delay_id: DelayId,
339 result: Result<(), ()>,
340 },
341 #[display("{result}: {child_execution_id}")] ChildExecutionFinished {
343 child_execution_id: ExecutionIdDerived,
344 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Version(2)))]
345 finished_version: Version,
346 #[cfg_attr(any(test, feature = "test"), arbitrary(value = crate::SUPPORTED_RETURN_VALUE_OK_EMPTY))]
347 result: SupportedFunctionReturnValue,
348 },
349}
350
351pub const DUMMY_CREATED: ExecutionRequest = ExecutionRequest::Created {
352 ffqn: FunctionFqn::new_static("", ""),
353 params: Params::empty(),
354 parent: None,
355 scheduled_at: DateTime::from_timestamp_nanos(0),
356 component_id: ComponentId::dummy_activity(),
357 deployment_id: DeploymentId::from_parts(0, 0),
358 metadata: ExecutionMetadata::empty(),
359 scheduled_by: None,
360};
361pub const DUMMY_HISTORY_EVENT: ExecutionRequest = ExecutionRequest::HistoryEvent {
362 event: HistoryEvent::JoinSetCreate {
363 join_set_id: JoinSetId {
364 kind: crate::JoinSetKind::OneOff,
365 name: StrVariant::empty(),
366 },
367 },
368};
369
370#[derive(
371 Clone,
372 derive_more::Debug,
373 derive_more::Display,
374 PartialEq,
375 Eq,
376 Serialize,
377 Deserialize,
378 schemars::JsonSchema,
379)]
380#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
381#[serde(rename_all = "snake_case")]
382pub enum ExecutionRequest {
383 #[display("Created({ffqn}, `{scheduled_at}`)")]
384 Created {
385 ffqn: FunctionFqn,
386 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Params::empty()))]
387 #[debug(skip)]
388 params: Params,
389 parent: Option<(ExecutionId, JoinSetId)>,
390 scheduled_at: DateTime<Utc>,
391 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity()))]
392 component_id: ComponentId,
393 deployment_id: DeploymentId,
394 #[cfg_attr(any(test, feature = "test"), arbitrary(default))]
395 metadata: ExecutionMetadata,
396 scheduled_by: Option<ExecutionId>,
397 },
398 Locked(Locked),
399 #[display("Unlocked({_0})")]
408 Unlocked(Unlocked),
409 #[display("ComponentUpgradeFinished({component_digest})")]
411 ComponentUpgradeFinished {
412 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity().component_digest))]
413 component_digest: ComponentDigest,
414 #[cfg_attr(any(test, feature = "test"), arbitrary(value = DeploymentId::from_parts(0, 0)))]
415 deployment_id: DeploymentId,
416 outcome: ComponentUpgradeOutcome,
417 },
418 #[display("TemporarilyFailed(`{backoff_expires_at}`)")]
421 TemporarilyFailed {
422 backoff_expires_at: DateTime<Utc>,
423 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
424 reason: StrVariant,
425 detail: Option<String>,
426 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
427 http_client_traces: Option<Vec<HttpClientTrace>>,
428 },
429 #[display("TemporarilyTimedOut(`{backoff_expires_at}`)")]
432 TemporarilyTimedOut {
433 backoff_expires_at: DateTime<Utc>,
434 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
435 http_client_traces: Option<Vec<HttpClientTrace>>,
436 },
437 #[display("Finished: {retval}")]
439 Finished {
440 #[cfg_attr(any(test, feature = "test"), arbitrary(value = crate::SUPPORTED_RETURN_VALUE_OK_EMPTY))]
441 retval: SupportedFunctionReturnValue,
442 #[cfg_attr(any(test, feature = "test"), arbitrary(value = None))]
443 http_client_traces: Option<Vec<HttpClientTrace>>,
444 },
445
446 #[display("HistoryEvent({event})")]
447 HistoryEvent {
448 event: HistoryEvent,
449 },
450 #[display("Paused")]
451 Paused,
452 #[display("Unpaused")]
453 Unpaused,
454 #[display("CancellationRequested")]
465 CancellationRequested,
466}
467
468#[derive(
470 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
471)]
472#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
473#[serde(tag = "type", rename_all = "snake_case")]
474pub enum ComponentUpgradeReason {
475 #[display("auto")]
476 Auto,
477 #[display("manual(force = {force})")]
478 Manual { force: bool },
479}
480
481#[derive(
482 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
483)]
484#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
485#[display("{reason}, pending at {unlocked_at}")]
486pub struct Unlocked {
487 #[serde(rename = "backoff_expires_at")] pub unlocked_at: DateTime<Utc>,
490 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
491 pub reason: StrVariant,
492}
493
494#[derive(
495 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
496)]
497#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
498#[serde(tag = "type", rename_all = "snake_case")]
499pub enum ComponentUpgradeOutcome {
500 #[display("success({reason})")]
501 Success { reason: ComponentUpgradeReason },
502 #[display("failed: {reason}")]
503 Failed {
504 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StrVariant::Static("reason")))]
505 reason: StrVariant,
506 },
507}
508
509impl ExecutionRequest {
510 #[must_use]
511 pub fn is_temporary_event(&self) -> bool {
512 matches!(
513 self,
514 Self::TemporarilyFailed { .. } | Self::TemporarilyTimedOut { .. }
515 )
516 }
517
518 #[must_use]
520 pub const fn variant(&self) -> &'static str {
521 match self {
522 ExecutionRequest::Created { .. } => "created",
523 ExecutionRequest::Locked(_) => "locked",
524 ExecutionRequest::Unlocked(_) => "unlocked",
525 ExecutionRequest::ComponentUpgradeFinished { .. } => "component_upgrade_finished",
526 ExecutionRequest::TemporarilyFailed { .. } => "temporarily_failed",
527 ExecutionRequest::TemporarilyTimedOut { .. } => "temporarily_timed_out",
528 ExecutionRequest::Finished { .. } => "finished",
529 ExecutionRequest::HistoryEvent { .. } => "history_event",
530 ExecutionRequest::Paused => "paused",
531 ExecutionRequest::Unpaused => "unpaused",
532 ExecutionRequest::CancellationRequested => "cancellation_requested",
533 }
534 }
535
536 #[must_use]
537 pub fn join_set_id(&self) -> Option<&JoinSetId> {
538 match self {
539 Self::Created {
540 parent: Some((_parent_id, join_set_id)),
541 ..
542 } => Some(join_set_id),
543 Self::HistoryEvent {
544 event:
545 HistoryEvent::JoinSetCreate { join_set_id, .. }
546 | HistoryEvent::JoinSetRequest { join_set_id, .. }
547 | HistoryEvent::JoinNext { join_set_id, .. },
548 } => Some(join_set_id),
549 _ => None,
550 }
551 }
552}
553
554#[derive(
555 Clone,
556 derive_more::Debug,
557 derive_more::Display,
558 PartialEq,
559 Eq,
560 Serialize,
561 Deserialize,
562 schemars::JsonSchema,
563)]
564#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
565#[display("Locked(`{lock_expires_at}`, {component_id})")]
566pub struct Locked {
567 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentId::dummy_activity()))]
568 pub component_id: ComponentId,
569 pub executor_id: ExecutorId,
570 pub deployment_id: DeploymentId,
571 pub run_id: RunId,
572 pub lock_expires_at: DateTime<Utc>,
573 #[cfg_attr(any(test, feature = "test"), arbitrary(value = ComponentRetryConfig::ZERO))]
574 pub retry_config: ComponentRetryConfig,
575}
576
577#[derive(
578 Debug,
579 Clone,
580 Copy,
581 PartialEq,
582 Eq,
583 derive_more::Display,
584 Serialize,
585 Deserialize,
586 schemars::JsonSchema,
587)]
588#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
589#[serde(tag = "type", rename_all = "snake_case")]
590pub enum PersistKind {
591 #[display("RandomU64({min}, {max_inclusive})")]
592 RandomU64 {
593 min: u64,
594 max_inclusive: u64,
595 },
596 #[display("RandomString({min_length}, {max_length_exclusive})")]
597 RandomString {
598 min_length: u64,
599 max_length_exclusive: u64,
600 },
601 ExecutionId,
602}
603
604#[must_use]
605pub fn from_u64_to_bytes(value: u64) -> [u8; 8] {
606 value.to_be_bytes()
607}
608
609#[derive(
610 derive_more::Debug,
611 Clone,
612 PartialEq,
613 Eq,
614 derive_more::Display,
615 Serialize,
616 Deserialize,
617 schemars::JsonSchema,
618)]
619#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
620#[serde(tag = "type", rename_all = "snake_case")]
621pub enum HistoryEvent {
623 #[display("Persist")]
625 Persist {
626 #[debug(skip)]
627 value: Vec<u8>, kind: PersistKind,
629 },
630 #[display("JoinSetCreate({join_set_id})")]
631 JoinSetCreate { join_set_id: JoinSetId },
632 #[display("JoinSetRequest({request})")]
633 JoinSetRequest {
635 join_set_id: JoinSetId,
636 request: JoinSetRequest,
637 },
638 #[display("JoinNext({join_set_id})")]
644 JoinNext {
645 join_set_id: JoinSetId,
646 run_expires_at: DateTime<Utc>,
649 requested_ffqn: Option<FunctionFqn>,
652 closing: bool,
654 },
655 #[display("JoinNextTry({join_set_id}, {outcome})")]
657 JoinNextTry {
658 join_set_id: JoinSetId,
659 outcome: JoinNextTryOutcome,
660 },
661 #[display("JoinNextTooMany({join_set_id})")]
663 JoinNextTooMany {
664 join_set_id: JoinSetId,
665 requested_ffqn: Option<FunctionFqn>,
668 },
669 #[display("Schedule({execution_id}, {schedule_at})")]
670 Schedule {
671 execution_id: ExecutionId,
672 schedule_at: HistoryEventScheduleAt, #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
674 result: Result<(), ScheduleRequestError>,
675 },
676 #[display("Stub({target_execution_id})")]
677 Stub {
678 target_execution_id: ExecutionIdDerived,
679 #[cfg_attr(any(test, feature = "test"), arbitrary(value = StubRetVal::Typed(crate::SUPPORTED_RETURN_VALUE_OK_EMPTY).hash()))]
680 retval_hash: StubRetValHash,
681 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
682 result: Result<(), StubError>,
683 },
684}
685
686#[derive(derive_more::Debug, Clone, PartialEq, Eq)]
688#[cfg_attr(any(test, feature = "test"), derive(Serialize, Deserialize))]
689#[cfg_attr(any(test, feature = "test"), serde(rename_all = "snake_case"))]
690pub enum StubRetVal {
691 Typed(SupportedFunctionReturnValue),
692 Untyped(String),
693}
694
695impl StubRetVal {
696 #[must_use]
698 pub fn hash(&self) -> StubRetValHash {
699 use sha2::{Digest as _, Sha256};
700 const STUB_RETVAL_HASH_VERSION: u8 = 1;
701 let mut hasher = Sha256::default();
702
703 match self {
704 StubRetVal::Typed(val) => {
705 hasher.update(b"T|");
706 let json = serde_json::to_string(val)
708 .expect("SupportedFunctionReturnValue is always serializable");
709 hasher.update(json.as_bytes());
710 }
711 StubRetVal::Untyped(s) => {
712 hasher.update(b"U|");
713 hasher.update(s.as_bytes());
714 }
715 }
716
717 let hash_bytes = hasher.finalize();
718 let mut result = [0u8; 33];
719 result[0] = STUB_RETVAL_HASH_VERSION;
720 result[1..].copy_from_slice(&hash_bytes);
721
722 StubRetValHash(result)
723 }
724}
725
726#[derive(
729 Clone,
730 PartialEq,
731 Eq,
732 serde_with::SerializeDisplay,
733 serde_with::DeserializeFromStr,
734 schemars::JsonSchema,
735)]
736#[schemars(with = "String")]
737pub struct StubRetValHash([u8; 33]);
738
739impl Display for StubRetValHash {
740 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
741 for b in self.0 {
742 write!(f, "{b:02x}")?;
743 }
744 Ok(())
745 }
746}
747
748impl Debug for StubRetValHash {
749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750 Display::fmt(self, f)
751 }
752}
753
754impl std::str::FromStr for StubRetValHash {
755 type Err = StubRetValHashParseError;
756
757 fn from_str(s: &str) -> Result<Self, Self::Err> {
758 if s.len() != 66 {
759 return Err(StubRetValHashParseError::InvalidLength(s.len()));
761 }
762 let mut bytes = [0u8; 33];
763 for i in 0..33 {
764 let chunk = &s[i * 2..i * 2 + 2];
765 bytes[i] =
766 u8::from_str_radix(chunk, 16).map_err(|_| StubRetValHashParseError::InvalidHex)?;
767 }
768 Ok(StubRetValHash(bytes))
769 }
770}
771
772#[derive(Debug, thiserror::Error)]
773pub enum StubRetValHashParseError {
774 #[error("invalid length: expected 66 hex chars, got {0}")]
775 InvalidLength(usize),
776 #[error("invalid hex character")]
777 InvalidHex,
778}
779
780#[derive(
783 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
784)]
785#[serde(rename_all = "snake_case")]
786pub enum StubError {
787 #[error("execution not found")]
788 ExecutionNotFound,
789 #[error("type check error: {0}")]
790 TypeCheckError(String),
791 #[error("conflict")]
792 Conflict,
793}
794
795#[derive(
797 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
798)]
799#[serde(rename_all = "snake_case")]
800pub enum ScheduleRequestError {
801 #[error("function not found")]
802 FunctionNotFound,
803 #[error("params parsing error: {0}")]
804 TypeCheckError(String),
805}
806
807#[derive(
809 Debug, Clone, thiserror::Error, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
810)]
811#[serde(rename_all = "snake_case")]
812pub enum ChildExecutionRequestError {
813 #[error("function not found")]
814 FunctionNotFound,
815 #[error("params parsing error: {0}")]
816 TypeCheckError(String),
817}
818
819#[derive(
820 Debug,
821 Clone,
822 Copy,
823 PartialEq,
824 Eq,
825 derive_more::Display,
826 Serialize,
827 Deserialize,
828 schemars::JsonSchema,
829)]
830#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
831#[serde(rename_all = "snake_case")]
832pub enum JoinNextTryOutcome {
833 #[display("found")]
835 Found,
836 #[display("pending")]
838 Pending,
839 #[display("all_processed")]
841 AllProcessed,
842}
843
844impl From<bool> for JoinNextTryOutcome {
845 fn from(found_response: bool) -> Self {
849 if found_response {
850 JoinNextTryOutcome::Found
851 } else {
852 JoinNextTryOutcome::Pending
853 }
854 }
855}
856
857#[derive(
858 Debug,
859 Clone,
860 Copy,
861 PartialEq,
862 Eq,
863 derive_more::Display,
864 Serialize,
865 Deserialize,
866 schemars::JsonSchema,
867)]
868#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
869#[serde(rename_all = "snake_case")]
870pub enum HistoryEventScheduleAt {
871 Now,
872 #[display("At(`{_0}`)")]
873 At(DateTime<Utc>),
874 #[display("In({_0:?})")]
875 In(Duration),
876}
877
878#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
879pub enum ScheduleAtConversionError {
880 #[error("source duration value is out of range")]
881 OutOfRangeError,
882}
883
884impl HistoryEventScheduleAt {
885 pub fn as_date_time(
886 &self,
887 now: DateTime<Utc>,
888 ) -> Result<DateTime<Utc>, ScheduleAtConversionError> {
889 match self {
890 Self::Now => Ok(now),
891 Self::At(date_time) => Ok(*date_time),
892 Self::In(duration) => {
893 let time_delta = TimeDelta::from_std(*duration)
894 .map_err(|_| ScheduleAtConversionError::OutOfRangeError)?;
895 now.checked_add_signed(time_delta)
896 .ok_or(ScheduleAtConversionError::OutOfRangeError)
897 }
898 }
899 }
900}
901
902#[derive(
903 Clone, Debug, PartialEq, Eq, derive_more::Display, Serialize, Deserialize, schemars::JsonSchema,
904)]
905#[cfg_attr(any(test, feature = "test"), derive(arbitrary::Arbitrary))]
906#[serde(tag = "type", rename_all = "snake_case")]
907pub enum JoinSetRequest {
908 #[display("DelayRequest({delay_id}, expires_at: `{expires_at}`, schedule_at: `{schedule_at}`)")]
910 DelayRequest {
911 delay_id: DelayId,
912 expires_at: DateTime<Utc>,
913 schedule_at: HistoryEventScheduleAt,
914 #[serde(default)]
915 paused: bool,
916 },
917 #[display("ChildExecutionRequest({child_execution_id}, {target_ffqn}, params: {params})")]
919 ChildExecutionRequest {
920 child_execution_id: ExecutionIdDerived,
921 target_ffqn: FunctionFqn,
922 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Params::empty()))]
923 params: Params,
924 #[cfg_attr(any(test, feature = "test"), arbitrary(value = Ok(())))]
925 result: Result<(), ChildExecutionRequestError>,
926 },
927}
928
929#[derive(Debug, Clone, thiserror::Error, derive_more::PartialEq, derive_more::Eq)]
931pub enum DbErrorGeneric {
932 #[error("database error: {reason}")]
933 Uncategorized {
934 reason: StrVariant,
935 #[eq(skip)]
936 #[partial_eq(skip)]
937 context: SpanTrace,
938 #[eq(skip)]
939 #[partial_eq(skip)]
940 #[source]
941 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
942 loc: &'static Location<'static>,
943 },
944 #[error("database was closed")]
945 Close,
946}
947
948#[derive(thiserror::Error, Clone, Debug, derive_more::PartialEq, derive_more::Eq)]
949pub enum DbErrorWriteNonRetriable {
950 #[error("validation failed: {0}")]
951 ValidationFailed(StrVariant),
952 #[error("conflict")]
953 Conflict,
954 #[error("already finished")]
955 AlreadyFinished,
956 #[error("illegal state: {reason}")]
957 IllegalState {
958 reason: StrVariant,
959 #[eq(skip)]
960 #[partial_eq(skip)]
961 context: SpanTrace,
962 #[eq(skip)]
963 #[partial_eq(skip)]
964 #[source]
965 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
966 loc: &'static Location<'static>,
967 },
968 #[error("illegal state: `Unlocked` cannot be appended in state {0}")]
969 UnlockedCannotBeAppended(&'static str),
970 #[error("version conflict: expected: {expected}, got: {requested}")]
971 VersionConflict {
972 expected: Version,
973 requested: Version,
974 },
975}
976
977#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
979pub enum DbErrorWrite {
980 #[error("cannot write - row not found")]
981 NotFound,
982 #[error("non-retriable error: {0}")]
983 NonRetriable(#[from] DbErrorWriteNonRetriable),
984 #[error(transparent)]
985 Generic(#[from] DbErrorGeneric),
986}
987
988#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
990pub enum DbErrorStubResponse {
991 #[error("stub conflict: already finished with a different value")]
992 StubConflict,
993 #[error(transparent)]
994 Write(#[from] DbErrorWrite),
995}
996
997#[derive(Debug, Clone, thiserror::Error, PartialEq)]
999pub enum DbErrorRead {
1000 #[error("cannot read - row not found")]
1001 NotFound,
1002 #[error(transparent)]
1003 Generic(#[from] DbErrorGeneric),
1004}
1005
1006#[derive(Debug, thiserror::Error, PartialEq)]
1007pub enum DbErrorReadWithTimeout {
1008 #[error("timeout")]
1009 Timeout(TimeoutOutcome),
1010 #[error(transparent)]
1011 DbErrorRead(#[from] DbErrorRead),
1012}
1013
1014pub type AppendResponse = Version;
1017pub type PendingExecution = (ExecutionId, Version, Params, Option<DateTime<Utc>>);
1018
1019#[derive(Debug, Clone)]
1020pub struct LockedExecution {
1021 pub execution_id: ExecutionId,
1022 pub next_version: Version,
1023 pub metadata: ExecutionMetadata,
1024 pub component_digest: ComponentDigest,
1025 pub locked_event: Locked,
1026 pub ffqn: FunctionFqn,
1027 pub params: Params,
1028 pub event_history: Vec<(HistoryEvent, Version)>,
1029 pub responses: Vec<ResponseWithCursor>,
1030 pub parent: Option<(ExecutionId, JoinSetId)>,
1031 pub intermittent_event_count: u32,
1032}
1033
1034pub type LockPendingResponse = Vec<LockedExecution>;
1035pub type AppendBatchResponse = Version;
1036
1037#[derive(Debug, Clone, PartialEq, derive_more::Display, Serialize, Deserialize)]
1038#[display("{event}")]
1039pub struct AppendRequest {
1040 pub created_at: DateTime<Utc>,
1041 pub event: ExecutionRequest,
1042}
1043
1044#[derive(Debug, Clone, PartialEq)]
1045#[cfg_attr(feature = "test", derive(Serialize))]
1046pub struct CreateRequest {
1047 pub created_at: DateTime<Utc>,
1048 pub execution_id: ExecutionId,
1049 pub ffqn: FunctionFqn,
1050 pub params: Params,
1051 pub parent: Option<(ExecutionId, JoinSetId)>,
1052 pub scheduled_at: DateTime<Utc>,
1053 pub component_id: ComponentId,
1054 pub deployment_id: DeploymentId,
1055 pub metadata: ExecutionMetadata,
1056 pub scheduled_by: Option<ExecutionId>,
1057 pub paused: bool,
1058}
1059
1060impl From<CreateRequest> for ExecutionRequest {
1061 fn from(value: CreateRequest) -> Self {
1062 Self::Created {
1063 ffqn: value.ffqn,
1064 params: value.params,
1065 parent: value.parent,
1066 scheduled_at: value.scheduled_at,
1067 component_id: value.component_id,
1068 deployment_id: value.deployment_id,
1069 metadata: value.metadata,
1070 scheduled_by: value.scheduled_by,
1071 }
1072 }
1073}
1074
1075#[async_trait]
1076pub trait DbPool: Send + Sync {
1077 async fn db_exec_conn(&self) -> Result<Box<dyn DbExecutor>, DbErrorGeneric>;
1078
1079 async fn connection(&self) -> Result<Box<dyn DbConnection>, DbErrorGeneric>;
1080
1081 async fn external_api_conn(&self) -> Result<Box<dyn DbExternalApi>, DbErrorGeneric>;
1082
1083 async fn cas_conn(&self) -> Result<Box<dyn crate::cas::Cas>, DbErrorGeneric>;
1087
1088 #[cfg(feature = "test")]
1089 async fn connection_test(&self) -> Result<Box<dyn DbConnectionTest>, DbErrorGeneric>;
1090}
1091
1092#[async_trait]
1093pub trait DbPoolCloseable {
1094 async fn close(&self);
1095}
1096
1097#[derive(Clone, Debug, PartialEq)]
1098#[cfg_attr(feature = "test", derive(Serialize))]
1099pub struct AppendEventsToExecution {
1100 pub execution_id: ExecutionId,
1101 pub version: Version,
1102 pub batch: Vec<AppendRequest>,
1103}
1104
1105#[derive(Clone, Debug, PartialEq)]
1106#[cfg_attr(feature = "test", derive(Serialize))]
1107pub struct AppendResponseToExecution {
1108 pub parent_execution_id: ExecutionId,
1109 pub created_at: DateTime<Utc>,
1110 pub join_set_id: JoinSetId,
1111 pub child_execution_id: ExecutionIdDerived,
1112 pub finished_version: Version,
1113 pub result: SupportedFunctionReturnValue,
1114}
1115
1116#[derive(Debug, Clone, PartialEq)]
1120#[cfg_attr(feature = "test", derive(Serialize))]
1121pub enum CapturedDbWrite {
1122 Append {
1123 execution_id: ExecutionId,
1124 version: Version,
1125 req: AppendRequest,
1126 backtraces: Vec<BacktraceInfo>,
1127 },
1128 AppendBatch {
1129 current_time: DateTime<Utc>,
1130 batch: Vec<AppendRequest>,
1131 execution_id: ExecutionId,
1132 version: Version,
1133 backtraces: Vec<BacktraceInfo>,
1134 },
1135 AppendBatchCreateNewExecution {
1136 current_time: DateTime<Utc>,
1137 batch: Vec<AppendRequest>,
1138 execution_id: ExecutionId,
1139 version: Version,
1140 child_req: Vec<CreateRequest>,
1141 backtraces: Vec<BacktraceInfo>,
1142 },
1143 AppendStubResponse {
1144 events: AppendEventsToExecution,
1145 response: AppendResponseToExecution,
1146 current_time: DateTime<Utc>,
1147 backtraces: Vec<BacktraceInfo>,
1148 },
1149 AppendFinished {
1150 execution_id: ExecutionId,
1151 version: Version,
1152 current_time: DateTime<Utc>,
1153 retval: SupportedFunctionReturnValue,
1154 parent: Option<(ExecutionId, JoinSetId)>,
1155 },
1156}
1157impl CapturedDbWrite {
1158 #[must_use]
1159 pub fn is_finished(&self) -> bool {
1160 matches!(self, CapturedDbWrite::AppendFinished { .. })
1161 }
1162}
1163
1164#[async_trait]
1165pub trait DbExecutor: Send + Sync {
1166 #[expect(clippy::too_many_arguments)]
1167 async fn lock_pending_by_ffqns(
1168 &self,
1169 batch_size: u32,
1170 pending_at_or_sooner: DateTime<Utc>,
1171 ffqns: Arc<[FunctionFqn]>,
1172 created_at: DateTime<Utc>,
1173 component_id: ComponentId,
1174 deployment_id: DeploymentId,
1175 executor_id: ExecutorId,
1176 lock_expires_at: DateTime<Utc>,
1177 run_id: RunId,
1178 retry_config: ComponentRetryConfig,
1179 ) -> Result<LockPendingResponse, DbErrorWrite>;
1180
1181 #[expect(clippy::too_many_arguments)]
1182 async fn lock_pending_by_ffqns_auto(
1183 &self,
1184 batch_size: u32,
1185 pending_at_or_sooner: DateTime<Utc>,
1186 ffqns: Arc<[FunctionFqn]>,
1187 created_at: DateTime<Utc>,
1188 component_id: ComponentId,
1189 deployment_id: DeploymentId,
1190 executor_id: ExecutorId,
1191 lock_expires_at: DateTime<Utc>,
1192 run_id: RunId,
1193 retry_config: ComponentRetryConfig,
1194 ) -> Result<LockPendingResponse, DbErrorWrite>;
1195
1196 #[expect(clippy::too_many_arguments)]
1197 async fn lock_pending_by_component_digest(
1198 &self,
1199 batch_size: u32,
1200 pending_at_or_sooner: DateTime<Utc>,
1201 component_id: &ComponentId,
1202 deployment_id: DeploymentId,
1203 created_at: DateTime<Utc>,
1204 executor_id: ExecutorId,
1205 lock_expires_at: DateTime<Utc>,
1206 run_id: RunId,
1207 retry_config: ComponentRetryConfig,
1208 ) -> Result<LockPendingResponse, DbErrorWrite>;
1209
1210 #[cfg(feature = "test")]
1211 #[expect(clippy::too_many_arguments)]
1212 async fn lock_one(
1213 &self,
1214 created_at: DateTime<Utc>,
1215 component_id: ComponentId,
1216 deployment_id: DeploymentId,
1217 execution_id: &ExecutionId,
1218 run_id: RunId,
1219 version: Version,
1220 executor_id: ExecutorId,
1221 lock_expires_at: DateTime<Utc>,
1222 retry_config: ComponentRetryConfig,
1223 ) -> Result<LockedExecution, DbErrorWrite>;
1224
1225 async fn append(
1228 &self,
1229 execution_id: ExecutionId,
1230 version: Version,
1231 req: AppendRequest,
1232 ) -> Result<AppendResponse, DbErrorWrite>;
1233
1234 async fn append_batch_respond_to_parent(
1237 &self,
1238 events: AppendEventsToExecution,
1239 response: AppendResponseToExecution,
1240 current_time: DateTime<Utc>, ) -> Result<AppendBatchResponse, DbErrorWrite>;
1242
1243 async fn wait_for_pending_by_ffqn(
1249 &self,
1250 pending_at_or_sooner: DateTime<Utc>,
1251 ffqns: Arc<[FunctionFqn]>,
1252 current_digest: Option<ComponentDigest>,
1253 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
1254 );
1255
1256 async fn wait_for_pending_by_component_digest(
1261 &self,
1262 pending_at_or_sooner: DateTime<Utc>,
1263 component_digest: &ComponentDigest,
1264 timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
1265 );
1266
1267 async fn cancel_activity_with_retries(
1268 &self,
1269 execution_id: &ExecutionId,
1270 cancelled_at: DateTime<Utc>,
1271 ) -> Result<CancelOutcome, DbErrorWrite> {
1272 let mut retries = 5;
1273 loop {
1274 match self
1275 .append_activity_cancellation_requested(execution_id, cancelled_at)
1276 .await
1277 {
1278 Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::VersionConflict {
1279 ..
1280 })) if retries > 0 => retries -= 1,
1281 res => return res,
1282 }
1283 }
1284 }
1285
1286 async fn cancel_workflow(
1291 &self,
1292 execution_id: &ExecutionId,
1293 cancelled_at: DateTime<Utc>,
1294 ) -> Result<CancelOutcome, DbErrorWrite>;
1295
1296 async fn cancel_workflow_with_retries(
1299 &self,
1300 execution_id: &ExecutionId,
1301 cancelled_at: DateTime<Utc>,
1302 ) -> Result<CancelOutcome, DbErrorWrite> {
1303 let mut retries = 5;
1304 loop {
1305 match self.cancel_workflow(execution_id, cancelled_at).await {
1306 Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::VersionConflict {
1307 ..
1308 })) if retries > 0 => retries -= 1,
1309 res => return res,
1310 }
1311 }
1312 }
1313
1314 async fn get_last_execution_event(
1316 &self,
1317 execution_id: &ExecutionId,
1318 ) -> Result<ExecutionEvent, DbErrorRead>;
1319
1320 async fn append_activity_cancellation_requested(
1321 &self,
1322 execution_id: &ExecutionId,
1323 cancelled_at: DateTime<Utc>,
1324 ) -> Result<CancelOutcome, DbErrorWrite>;
1325}
1326
1327pub enum AppendDelayResponseOutcome {
1328 Success,
1329 AlreadyFinished,
1330 AlreadyCancelled,
1331}
1332
1333#[derive(Debug, Clone, Default)]
1334pub struct ListExecutionsFilter {
1335 pub function_name_filter: Option<FunctionNameFilter>,
1336 pub show_derived: bool,
1337 pub hide_finished: bool,
1338 pub execution_id_prefix: Option<String>,
1339 pub component_digest: Option<ComponentDigest>,
1340 pub deployment_id: Option<DeploymentId>,
1341 pub state_filters: Vec<ExecutionStateFilter>,
1345}
1346
1347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1349pub enum ExecutionStateFilter {
1350 Locked,
1351 Pending {
1353 now: DateTime<Utc>,
1354 },
1355 Scheduled {
1357 now: DateTime<Utc>,
1358 },
1359 Blocked,
1360 Paused,
1362 Cancelling,
1364 Finished,
1366 FinishedOk,
1368 FinishedError,
1370 FinishedExecutionFailure,
1372}
1373
1374#[derive(Debug, Clone, PartialEq, Eq)]
1375pub enum FunctionNameFilter {
1376 PackageName(String),
1377 InterfaceName(String),
1378 FunctionName(String),
1379}
1380
1381impl FunctionNameFilter {
1382 #[must_use]
1383 pub fn like_pattern(&self) -> String {
1384 match self {
1385 Self::FunctionName(function_name) | Self::InterfaceName(function_name) => {
1386 format!("{function_name}%")
1387 }
1388 Self::PackageName(package_name) => {
1389 if let Some((pkg_fqn_without_version, version)) = package_name.rsplit_once('@')
1390 && !version.is_empty()
1391 && pkg_fqn_without_version.contains(':')
1392 {
1393 format!("{pkg_fqn_without_version}/%@{version}.%")
1394 } else {
1395 format!("{package_name}%")
1396 }
1397 }
1398 }
1399 }
1400}
1401
1402#[async_trait]
1403pub trait DbExternalApi: DbConnection {
1404 async fn get_backtrace(
1406 &self,
1407 execution_id: &ExecutionId,
1408 filter: BacktraceFilter,
1409 ) -> Result<BacktraceInfo, DbErrorRead>;
1410
1411 async fn upsert_source_mapping(
1417 &self,
1418 component_digest: &ComponentDigest,
1419 frame_key: &str,
1420 is_suffix: bool,
1421 digest: &ContentDigest,
1422 ) -> Result<(), DbErrorWrite>;
1423
1424 async fn resolve_source_digest(
1429 &self,
1430 component_digest: &ComponentDigest,
1431 file: &str,
1432 ) -> Result<Option<ContentDigest>, DbErrorRead>;
1433
1434 async fn upsert_component_metadata(
1436 &self,
1437 records: Vec<ComponentMetadataRecord>,
1438 ) -> Result<(), DbErrorWrite>;
1439
1440 async fn insert_deployment_components(
1442 &self,
1443 deployment_id: DeploymentId,
1444 records: Vec<DeploymentComponentRecord>,
1445 ) -> Result<(), DbErrorWrite>;
1446
1447 async fn list_deployment_components(
1449 &self,
1450 deployment_id: DeploymentId,
1451 ) -> Result<Vec<DeploymentComponentDetail>, DbErrorRead>;
1452
1453 async fn get_deployment_component_wit(
1455 &self,
1456 deployment_id: DeploymentId,
1457 component_digest: &ComponentDigest,
1458 ) -> Result<Option<String>, DbErrorRead>;
1459
1460 async fn list_executions(
1462 &self,
1463 filter: ListExecutionsFilter,
1464 pagination: ExecutionListPagination,
1465 ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric>;
1466
1467 async fn list_execution_events(
1472 &self,
1473 execution_id: &ExecutionId,
1474 pagination: Pagination<VersionType>,
1475 include_backtrace_id: bool,
1476 ) -> Result<ListExecutionEventsResponse, DbErrorRead>;
1477
1478 async fn list_responses(
1487 &self,
1488 execution_id: &ExecutionId,
1489 pagination: Pagination<u32>,
1490 ) -> Result<ListResponsesResponse, DbErrorRead>;
1491
1492 async fn list_execution_events_responses(
1493 &self,
1494 execution_id: &ExecutionId,
1495 req_since: &Version,
1496 req_max_length: VersionType,
1497 req_include_backtrace_id: bool,
1498 resp_pagination: Pagination<VersionType>,
1499 ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead>;
1500
1501 async fn upgrade_execution_component(
1502 &self,
1503 execution_id: &ExecutionId,
1504 old: &ComponentDigest,
1505 new: &ComponentDigest,
1506 reason: ComponentUpgradeReason,
1507 ) -> Result<(), DbErrorWrite>;
1508
1509 async fn list_logs(
1510 &self,
1511 execution_id: &ExecutionId,
1512 show_derived: bool,
1513 filter: LogFilter,
1514 pagination: Pagination<LogCursor>,
1515 ) -> Result<ListLogsResponse, DbErrorRead>;
1516
1517 async fn list_deployment_states(
1518 &self,
1519 current_time: DateTime<Utc>,
1520 pagination: Pagination<Option<DeploymentId>>,
1521 include_deployment_toml: bool,
1522 execution_counts: DeploymentExecutionCounts,
1523 ) -> Result<Vec<DeploymentState>, DbErrorRead>;
1524
1525 async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite>;
1528
1529 async fn insert_deployment_with_components(
1534 &self,
1535 record: DeploymentRecord,
1536 component_metadata: Vec<ComponentMetadataRecord>,
1537 deployment_components: Vec<DeploymentComponentRecord>,
1538 ) -> Result<(), DbErrorWrite>;
1539
1540 async fn missing_digests(
1545 &self,
1546 deployment_id: DeploymentId,
1547 ) -> Result<Vec<ContentDigest>, DbErrorRead>;
1548
1549 async fn list_deployment_files(
1551 &self,
1552 deployment_id: DeploymentId,
1553 ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead>;
1554
1555 async fn gc_orphan_files(&self) -> Result<u64, DbErrorWrite>;
1559
1560 async fn activate_deployment(
1561 &self,
1562 deployment_id: DeploymentId,
1563 now: DateTime<Utc>,
1564 ) -> Result<(), DbErrorWrite>;
1565
1566 async fn enqueue_deployment(
1571 &self,
1572 deployment_id: DeploymentId,
1573 ) -> Result<EnqueueOutcome, DbErrorWrite>;
1574
1575 async fn get_deployment(
1577 &self,
1578 deployment_id: DeploymentId,
1579 ) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1580
1581 #[cfg(feature = "test")]
1584 async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1585
1586 async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1589
1590 async fn list_deployments(
1591 &self,
1592 pagination: Pagination<Option<DeploymentId>>,
1593 ) -> Result<Vec<DeploymentRecord>, DbErrorRead>;
1594
1595 async fn pause_execution(
1599 &self,
1600 execution_id: &ExecutionId,
1601 paused_at: DateTime<Utc>,
1602 ) -> Result<AppendResponse, DbErrorWrite>;
1603
1604 async fn unpause_execution(
1606 &self,
1607 execution_id: &ExecutionId,
1608 unpaused_at: DateTime<Utc>,
1609 ) -> Result<AppendResponse, DbErrorWrite>;
1610
1611 async fn pause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite>;
1615
1616 async fn unpause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite>;
1620}
1621pub const LIST_DEPLOYMENT_STATES_DEFAULT_LENGTH: u16 = 20;
1622pub const LIST_DEPLOYMENT_STATES_DEFAULT_PAGINATION: Pagination<Option<DeploymentId>> =
1623 Pagination::OlderThan {
1624 length: LIST_DEPLOYMENT_STATES_DEFAULT_LENGTH,
1625 cursor: None,
1626 including_cursor: false,
1627 };
1628
1629pub struct DeploymentState {
1630 pub deployment_id: DeploymentId,
1631 pub description: Option<String>,
1632 pub digest: ContentDigest,
1634 pub locked: u32,
1635 pub pending: u32,
1637 pub scheduled: u32,
1639 pub blocked: u32,
1640 pub paused: u32,
1642 pub cancelling: u32,
1644 pub finished_ok: u32,
1645 pub finished_error: u32,
1646 pub finished_execution_failure: u32,
1647 pub deployment_toml: Option<String>,
1649 pub created_at: DateTime<Utc>,
1650 pub last_active_at: Option<DateTime<Utc>>,
1652 pub status: DeploymentStatus,
1653}
1654
1655#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1656pub enum DeploymentExecutionCounts {
1657 Skip,
1659 Count { include_derived: bool },
1661}
1662
1663#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1664pub enum DeploymentStatus {
1665 Inactive,
1666 Enqueued,
1668 Active,
1669}
1670
1671#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1673pub enum EnqueueOutcome {
1674 Enqueued,
1676 AlreadyActive,
1679}
1680
1681impl DeploymentStatus {
1682 #[must_use]
1683 pub fn as_str(&self) -> &'static str {
1684 match self {
1685 DeploymentStatus::Inactive => "inactive",
1686 DeploymentStatus::Enqueued => "enqueued",
1687 DeploymentStatus::Active => "active",
1688 }
1689 }
1690}
1691
1692impl std::str::FromStr for DeploymentStatus {
1693 type Err = StrVariant;
1694 fn from_str(s: &str) -> Result<Self, Self::Err> {
1695 match s {
1696 "inactive" => Ok(DeploymentStatus::Inactive),
1697 "enqueued" => Ok(DeploymentStatus::Enqueued),
1698 "active" => Ok(DeploymentStatus::Active),
1699 _ => Err(StrVariant::from(format!("unknown deployment status: {s}"))),
1700 }
1701 }
1702}
1703
1704#[derive(Debug, Clone)]
1705pub struct DeploymentRecord {
1706 pub deployment_id: DeploymentId,
1707 pub description: Option<String>,
1708 pub digest: ContentDigest,
1710 pub created_at: DateTime<Utc>,
1711 pub last_active_at: Option<DateTime<Utc>>,
1713 pub status: DeploymentStatus,
1714 pub deployment_toml: String, pub obelisk_version: String,
1716 pub created_by: Option<String>,
1717 pub files: Vec<DeploymentFileRecord>,
1718}
1719
1720impl DeploymentRecord {
1721 #[must_use]
1723 pub fn compute_digest(deployment_toml: &str) -> ContentDigest {
1724 use sha2::{Digest as _, Sha256};
1725 let hash: [u8; 32] = Sha256::digest(deployment_toml.as_bytes()).into();
1726 ContentDigest(crate::component_id::Digest(hash))
1727 }
1728}
1729
1730#[derive(Debug, Clone, PartialEq, Eq)]
1731pub struct DeploymentFileRecord {
1732 pub path: String,
1733 pub digest: ContentDigest,
1734}
1735
1736#[derive(Debug, Clone)]
1737pub struct ComponentMetadataRecord {
1738 pub component_digest: ComponentDigest,
1739 pub imports: Vec<PersistedFunctionMetadata>,
1740 pub exports: Vec<PersistedFunctionMetadata>,
1741 pub wit: String,
1742 pub wit_origin: String,
1743}
1744
1745#[derive(Debug, Clone)]
1747pub struct DeploymentComponentRecord {
1748 pub deployment_id: DeploymentId,
1749 pub component_name: StrVariant,
1750 pub component_digest: ComponentDigest,
1751 pub component_type: ComponentType,
1752}
1753
1754#[derive(Debug, Clone)]
1755pub struct DeploymentComponentDetail {
1756 pub component_id: ComponentId,
1757 pub imports: Vec<PersistedFunctionMetadata>,
1758 pub exports: Vec<PersistedFunctionMetadata>,
1759 pub wit: String,
1760}
1761
1762#[derive(
1763 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, schemars::JsonSchema,
1764)]
1765pub struct PersistedFunctionMetadata {
1766 pub ffqn: FunctionFqn,
1767 pub parameter_types: Vec<PersistedParameterType>,
1768 pub return_type: String,
1769 pub extension: Option<FunctionExtension>,
1770 pub submittable: bool,
1771}
1772
1773#[derive(
1774 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, schemars::JsonSchema,
1775)]
1776pub struct PersistedParameterType {
1777 pub name: String,
1778 pub wit_type: String,
1779}
1780
1781impl From<FunctionMetadata> for PersistedFunctionMetadata {
1782 fn from(value: FunctionMetadata) -> Self {
1783 PersistedFunctionMetadata {
1784 ffqn: value.ffqn,
1785 parameter_types: value
1786 .parameter_types
1787 .0
1788 .into_iter()
1789 .map(|param| PersistedParameterType {
1790 name: param.name.to_string(),
1791 wit_type: param.wit_type.to_string(),
1792 })
1793 .collect(),
1794 return_type: value.return_type.wit_type().to_string(),
1795 extension: value.extension,
1796 submittable: value.submittable,
1797 }
1798 }
1799}
1800
1801#[derive(Debug)]
1802pub struct ListLogsResponse {
1803 pub items: Vec<LogEntryRow>,
1804 pub next_page: Pagination<LogCursor>, pub prev_page: Option<Pagination<LogCursor>>, }
1807
1808#[derive(Debug)]
1809pub struct LogFilter {
1810 show_logs: bool,
1811 show_streams: bool,
1812 levels: Vec<LogLevel>, stream_types: Vec<LogStreamType>, created_after: Option<DateTime<Utc>>,
1815 created_before: Option<DateTime<Utc>>,
1816}
1817impl LogFilter {
1818 #[must_use]
1820 pub fn show_logs(levels: Vec<LogLevel>) -> LogFilter {
1821 LogFilter {
1822 show_logs: true,
1823 show_streams: false,
1824 levels,
1825 stream_types: Vec::new(),
1826 created_after: None,
1827 created_before: None,
1828 }
1829 }
1830 #[must_use]
1832 pub fn show_streams(stream_types: Vec<LogStreamType>) -> LogFilter {
1833 LogFilter {
1834 show_logs: false,
1835 show_streams: true,
1836 levels: Vec::new(),
1837 stream_types,
1838 created_after: None,
1839 created_before: None,
1840 }
1841 }
1842 #[must_use]
1844 pub fn show_combined(levels: Vec<LogLevel>, stream_types: Vec<LogStreamType>) -> LogFilter {
1845 LogFilter {
1846 show_logs: true,
1847 show_streams: true,
1848 levels,
1849 stream_types,
1850 created_after: None,
1851 created_before: None,
1852 }
1853 }
1854 #[must_use]
1856 pub fn should_show_logs(&self) -> bool {
1857 self.show_logs
1858 }
1859 #[must_use]
1860 pub fn should_show_streams(&self) -> bool {
1861 self.show_streams
1862 }
1863 #[must_use]
1864 pub fn levels(&self) -> &Vec<LogLevel> {
1865 &self.levels
1866 }
1867 #[must_use]
1868 pub fn stream_types(&self) -> &Vec<LogStreamType> {
1869 &self.stream_types
1870 }
1871 #[must_use]
1872 pub fn with_created_bounds(
1873 mut self,
1874 created_after: Option<DateTime<Utc>>,
1875 created_before: Option<DateTime<Utc>>,
1876 ) -> Self {
1877 self.created_after = created_after;
1878 self.created_before = created_before;
1879 self
1880 }
1881 #[must_use]
1882 pub fn created_after(&self) -> Option<DateTime<Utc>> {
1883 self.created_after
1884 }
1885 #[must_use]
1886 pub fn created_before(&self) -> Option<DateTime<Utc>> {
1887 self.created_before
1888 }
1889}
1890
1891#[derive(Debug, Clone)]
1892pub struct ExecutionWithStateRequestsResponses {
1893 pub execution_with_state: ExecutionWithState,
1894 pub events: Vec<ExecutionEvent>,
1895 pub responses: Vec<ResponseWithCursor>,
1896 pub max_version: Version,
1897 pub max_cursor: ResponseCursor,
1898}
1899
1900#[async_trait]
1901pub trait DbConnection: DbExecutor {
1902 async fn get(&self, execution_id: &ExecutionId) -> Result<ExecutionLog, DbErrorRead>;
1904
1905 async fn get_cancelling(&self, batch_size: u32) -> Result<Vec<ExecutionId>, DbErrorRead>;
1910
1911 async fn append_delay_response(
1912 &self,
1913 created_at: DateTime<Utc>,
1914 execution_id: ExecutionId,
1915 join_set_id: JoinSetId,
1916 delay_id: DelayId,
1917 outcome: Result<(), ()>, ) -> Result<AppendDelayResponseOutcome, DbErrorWrite>;
1919
1920 async fn append_batch(
1923 &self,
1924 current_time: DateTime<Utc>, batch: Vec<AppendRequest>,
1926 execution_id: ExecutionId,
1927 version: Version,
1928 ) -> Result<AppendBatchResponse, DbErrorWrite>;
1929
1930 async fn append_batch_create_new_execution(
1933 &self,
1934 current_time: DateTime<Utc>, batch: Vec<AppendRequest>, execution_id: ExecutionId,
1937 version: Version,
1938 child_req: Vec<CreateRequest>,
1939 backtraces: Vec<BacktraceInfo>,
1940 ) -> Result<AppendBatchResponse, DbErrorWrite>;
1941
1942 async fn get_execution_event(
1944 &self,
1945 execution_id: &ExecutionId,
1946 version: &Version,
1947 ) -> Result<ExecutionEvent, DbErrorRead>;
1948
1949 async fn upsert_stub_response(
1953 &self,
1954 execution_id: ExecutionIdDerived,
1955 version: Version,
1956 req: AppendRequest,
1957 response: AppendResponseToExecution,
1958 current_time: DateTime<Utc>,
1959 ) -> Result<(), DbErrorStubResponse>;
1960
1961 #[instrument(skip(self))]
1962 async fn get_create_request(
1963 &self,
1964 execution_id: &ExecutionId,
1965 ) -> Result<CreateRequest, DbErrorRead> {
1966 let execution_event = self
1967 .get_execution_event(execution_id, &Version::new(0))
1968 .await?;
1969 if let ExecutionRequest::Created {
1970 ffqn,
1971 params,
1972 parent,
1973 scheduled_at,
1974 component_id,
1975 deployment_id,
1976 metadata,
1977 scheduled_by,
1978 } = execution_event.event
1979 {
1980 Ok(CreateRequest {
1981 created_at: execution_event.created_at,
1982 execution_id: execution_id.clone(),
1983 ffqn,
1984 params,
1985 parent,
1986 scheduled_at,
1987 component_id,
1988 deployment_id,
1989 metadata,
1990 scheduled_by,
1991 paused: false,
1992 })
1993 } else {
1994 Err(DbErrorRead::Generic(DbErrorGeneric::Uncategorized {
1995 reason: "execution log must start with creation".into(),
1996 context: SpanTrace::capture(),
1997 source: None,
1998 loc: Location::caller(),
1999 }))
2000 }
2001 }
2002
2003 async fn get_pending_state(
2004 &self,
2005 execution_id: &ExecutionId,
2006 ) -> Result<ExecutionWithState, DbErrorRead>;
2007
2008 async fn get_expired_timers(
2010 &self,
2011 at: DateTime<Utc>,
2012 ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric>;
2013
2014 async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite>;
2016
2017 async fn subscribe_to_next_responses(
2024 &self,
2025 execution_id: &ExecutionId,
2026 last_response: ResponseCursor,
2027 timeout_fut: Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>,
2028 ) -> Result<Vec<ResponseWithCursor>, DbErrorReadWithTimeout>;
2029
2030 async fn wait_for_finished_result(
2037 &self,
2038 execution_id: &ExecutionId,
2039 timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
2040 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout>;
2041
2042 async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite>;
2043
2044 async fn append_backtrace_batch(&self, batch: Vec<BacktraceInfo>) -> Result<(), DbErrorWrite>;
2045
2046 async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite>;
2047
2048 async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite>;
2049
2050 #[cfg(feature = "test")]
2052 async fn get_finished_result(
2053 &self,
2054 execution_id: &ExecutionId,
2055 ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
2056 self.wait_for_finished_result(
2057 execution_id,
2058 Some(Box::pin(std::future::ready(TimeoutOutcome::Timeout))),
2059 )
2060 .await
2061 }
2062}
2063
2064#[derive(Clone, Debug)]
2065pub struct LogInfoAppendRow {
2066 pub execution_id: ExecutionId,
2067 pub run_id: RunId,
2068 pub log_entry: LogEntry,
2069}
2070
2071#[derive(Debug, Clone)]
2072pub struct LogEntryRow {
2073 pub cursor: LogCursor,
2074 pub run_id: RunId,
2075 pub log_entry: LogEntry,
2076 pub execution_id: ExecutionId,
2077}
2078
2079#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2080pub struct LogCursor(pub i64);
2081
2082#[derive(Debug, Clone)]
2083pub enum LogEntry {
2084 Log {
2085 created_at: DateTime<Utc>,
2086 level: LogLevel,
2087 message: String,
2088 },
2089 Stream {
2090 created_at: DateTime<Utc>,
2091 payload: Vec<u8>,
2092 stream_type: LogStreamType,
2093 },
2094}
2095impl LogEntry {
2096 #[must_use]
2097 pub fn created_at(&self) -> DateTime<Utc> {
2098 match self {
2099 LogEntry::Log { created_at, .. } | LogEntry::Stream { created_at, .. } => *created_at,
2100 }
2101 }
2102}
2103
2104#[derive(
2105 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, derive_more::TryFrom, strum::EnumIter,
2106)]
2107#[try_from(repr)]
2108#[repr(u8)]
2109pub enum LogLevel {
2110 Trace = 1,
2111 Debug,
2112 Info,
2113 Warn,
2114 Error,
2115}
2116#[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::TryFrom, strum::EnumIter)]
2117#[try_from(repr)]
2118#[repr(u8)]
2119pub enum LogStreamType {
2120 StdOut = 1,
2121 StdErr,
2122}
2123
2124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2125pub enum TimeoutOutcome {
2126 Timeout,
2127 Cancel,
2128}
2129
2130#[cfg(feature = "test")]
2131#[async_trait]
2132pub trait DbConnectionTest: DbConnection {
2133 async fn append_response(
2134 &self,
2135 created_at: DateTime<Utc>,
2136 execution_id: ExecutionId,
2137 response_event: JoinSetResponseEvent,
2138 ) -> Result<(), DbErrorWrite>;
2139}
2140
2141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2142pub enum CancelOutcome {
2143 Cancelled,
2144 AlreadyFinished,
2145 AlreadyCancelling,
2146}
2147
2148#[instrument(skip(db_connection))]
2149pub async fn stub_execution(
2150 db_connection: &dyn DbConnection,
2151 execution_id: ExecutionIdDerived,
2152 parent_execution_id: ExecutionId,
2153 join_set_id: JoinSetId,
2154 created_at: DateTime<Utc>,
2155 return_value: SupportedFunctionReturnValue,
2156) -> Result<(), DbErrorWrite> {
2157 let stub_finished_version = Version::new(1); let finished_req = AppendRequest {
2159 created_at,
2160 event: ExecutionRequest::Finished {
2161 retval: return_value.clone(),
2162 http_client_traces: None,
2163 },
2164 };
2165 db_connection
2166 .upsert_stub_response(
2167 execution_id.clone(),
2168 stub_finished_version.clone(),
2169 finished_req,
2170 AppendResponseToExecution {
2171 parent_execution_id,
2172 created_at,
2173 join_set_id,
2174 child_execution_id: execution_id,
2175 finished_version: stub_finished_version,
2176 result: return_value,
2177 },
2178 created_at,
2179 )
2180 .await
2181 .map_err(|err| match err {
2182 DbErrorStubResponse::StubConflict => {
2183 DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)
2184 }
2185 DbErrorStubResponse::Write(db_err) => db_err,
2186 })
2187}
2188
2189pub async fn cancel_delay(
2190 db_connection: &dyn DbConnection,
2191 delay_id: DelayId,
2192 cancelled_at: DateTime<Utc>,
2193) -> Result<CancelOutcome, DbErrorWrite> {
2194 let (parent_execution_id, join_set_id) = delay_id.split_to_parts();
2195 db_connection
2196 .append_delay_response(
2197 cancelled_at,
2198 parent_execution_id,
2199 join_set_id,
2200 delay_id,
2201 Err(()), )
2203 .await
2204 .map(|ok| match ok {
2205 AppendDelayResponseOutcome::Success | AppendDelayResponseOutcome::AlreadyCancelled => {
2206 CancelOutcome::Cancelled
2207 }
2208 AppendDelayResponseOutcome::AlreadyFinished => CancelOutcome::AlreadyFinished,
2209 })
2210}
2211
2212#[derive(Clone, Debug)]
2213pub enum BacktraceFilter {
2214 First,
2215 Last,
2216 Specific(Version),
2217}
2218
2219#[derive(Clone, Debug, PartialEq, Eq)]
2220#[cfg_attr(feature = "test", derive(Serialize))]
2221pub struct BacktraceInfo {
2222 pub execution_id: ExecutionId,
2223 pub component_id: ComponentId,
2224 pub version_min_including: Version,
2225 pub version_max_excluding: Version,
2226 pub wasm_backtrace: WasmBacktrace,
2227}
2228
2229#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2230pub struct WasmBacktrace {
2231 pub frames: Vec<FrameInfo>,
2232}
2233
2234#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2235pub struct FrameInfo {
2236 pub module: String,
2237 pub func_name: String,
2238 pub symbols: Vec<FrameSymbol>,
2239}
2240
2241#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, schemars::JsonSchema)]
2242pub struct FrameSymbol {
2243 pub func_name: Option<String>,
2244 pub file: Option<String>,
2245 pub line: Option<u32>,
2246 pub col: Option<u32>,
2247}
2248
2249mod wasm_backtrace {
2250 use super::{FrameInfo, FrameSymbol, WasmBacktrace};
2251
2252 impl WasmBacktrace {
2253 pub fn maybe_from(backtrace: &wasmtime::WasmBacktrace) -> Option<Self> {
2254 if backtrace.frames().is_empty() {
2255 None
2256 } else {
2257 Some(Self {
2258 frames: backtrace.frames().iter().map(FrameInfo::from).collect(),
2259 })
2260 }
2261 }
2262 }
2263
2264 impl From<&wasmtime::FrameInfo> for FrameInfo {
2265 fn from(frame: &wasmtime::FrameInfo) -> Self {
2266 let module_name = frame.module().name().unwrap_or("<unknown>").to_string();
2267 let mut func_name = String::new();
2268 wasmtime_environ::demangle_function_name_or_index(
2269 &mut func_name,
2270 frame.func_name(),
2271 frame.func_index() as usize,
2272 )
2273 .expect("writing to string must succeed");
2274 Self {
2275 module: module_name,
2276 func_name,
2277 symbols: frame
2278 .symbols()
2279 .iter()
2280 .map(std::convert::Into::into)
2281 .collect(),
2282 }
2283 }
2284 }
2285
2286 impl From<&wasmtime::FrameSymbol> for FrameSymbol {
2287 fn from(symbol: &wasmtime::FrameSymbol) -> Self {
2288 let func_name = symbol.name().map(|name| {
2289 let mut writer = String::new();
2290 wasmtime_environ::demangle_function_name(&mut writer, name)
2291 .expect("writing to string must succeed");
2292 writer
2293 });
2294
2295 Self {
2296 func_name,
2297 file: symbol.file().map(ToString::to_string),
2298 line: symbol.line(),
2299 col: symbol.column(),
2300 }
2301 }
2302 }
2303}
2304#[derive(Debug, Clone, derive_more::Display)]
2305#[display("{execution_id} {pending_state} {component_digest}")]
2306pub struct ExecutionWithState {
2307 pub execution_id: ExecutionId,
2308 pub ffqn: FunctionFqn,
2309 pub pending_state: PendingState,
2310 pub created_at: DateTime<Utc>,
2311 pub first_scheduled_at: DateTime<Utc>,
2312 pub component_digest: ComponentDigest,
2313 pub component_type: ComponentType,
2314 pub deployment_id: DeploymentId,
2315}
2316
2317#[derive(Debug, Clone)]
2318pub enum ExecutionListPagination {
2319 CreatedBy(Pagination<Option<DateTime<Utc>>>),
2320 ExecutionId(Pagination<Option<ExecutionId>>),
2321}
2322impl Default for ExecutionListPagination {
2323 fn default() -> ExecutionListPagination {
2324 ExecutionListPagination::CreatedBy(Pagination::OlderThan {
2325 length: 20,
2326 cursor: None,
2327 including_cursor: false, })
2329 }
2330}
2331impl ExecutionListPagination {
2332 #[must_use]
2333 pub fn length(&self) -> u16 {
2334 match self {
2335 ExecutionListPagination::CreatedBy(pagination) => pagination.length(),
2336 ExecutionListPagination::ExecutionId(pagination) => pagination.length(),
2337 }
2338 }
2339}
2340
2341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2342pub enum Pagination<T> {
2343 NewerThan {
2344 length: u16,
2345 cursor: T,
2346 including_cursor: bool,
2347 },
2348 OlderThan {
2349 length: u16,
2350 cursor: T,
2351 including_cursor: bool,
2352 },
2353}
2354impl<T: Clone> Pagination<T> {
2355 pub fn length(&self) -> u16 {
2356 match self {
2357 Pagination::NewerThan { length, .. } | Pagination::OlderThan { length, .. } => *length,
2358 }
2359 }
2360
2361 pub fn rel(&self) -> &'static str {
2362 match self {
2363 Pagination::NewerThan {
2364 including_cursor: false,
2365 ..
2366 } => ">",
2367 Pagination::NewerThan {
2368 including_cursor: true,
2369 ..
2370 } => ">=",
2371 Pagination::OlderThan {
2372 including_cursor: false,
2373 ..
2374 } => "<",
2375 Pagination::OlderThan {
2376 including_cursor: true,
2377 ..
2378 } => "<=",
2379 }
2380 }
2381
2382 pub fn is_desc(&self) -> bool {
2383 matches!(self, Pagination::OlderThan { .. })
2384 }
2385
2386 pub fn asc_or_desc(&self) -> &'static str {
2387 if self.is_asc() { "asc" } else { "desc" }
2388 }
2389
2390 pub fn is_asc(&self) -> bool {
2391 !self.is_desc()
2392 }
2393
2394 pub fn cursor(&self) -> &T {
2395 match self {
2396 Pagination::NewerThan { cursor, .. } | Pagination::OlderThan { cursor, .. } => cursor,
2397 }
2398 }
2399
2400 #[must_use]
2401 pub fn invert(&self) -> Self {
2402 match self {
2403 Pagination::NewerThan {
2404 length,
2405 cursor,
2406 including_cursor,
2407 } => Pagination::OlderThan {
2408 length: *length,
2409 cursor: cursor.clone(),
2410 including_cursor: !including_cursor,
2411 },
2412 Pagination::OlderThan {
2413 length,
2414 cursor,
2415 including_cursor,
2416 } => Pagination::NewerThan {
2417 length: *length,
2418 cursor: cursor.clone(),
2419 including_cursor: !including_cursor,
2420 },
2421 }
2422 }
2423}
2424
2425#[cfg(feature = "test")]
2426pub async fn wait_for_pending_state_fn<T: Debug>(
2427 db_connection: &dyn DbConnectionTest,
2428 execution_id: &ExecutionId,
2429 predicate: impl Fn(ExecutionLog) -> Option<T> + Send,
2430 timeout: Option<Duration>,
2431) -> Result<T, DbErrorReadWithTimeout> {
2432 tracing::trace!(%execution_id, "Waiting for predicate");
2433 let fut = async move {
2434 loop {
2435 let execution_log = db_connection.get(execution_id).await?;
2436 if let Some(t) = predicate(execution_log) {
2437 tracing::debug!(%execution_id, "Found: {t:?}");
2438 return Ok(t);
2439 }
2440 tokio::time::sleep(Duration::from_millis(10)).await;
2441 }
2442 };
2443
2444 if let Some(timeout) = timeout {
2445 tokio::select! { res = fut => res,
2447 () = tokio::time::sleep(timeout) => Err(DbErrorReadWithTimeout::Timeout(TimeoutOutcome::Timeout))
2448 }
2449 } else {
2450 fut.await
2451 }
2452}
2453
2454#[derive(Debug, Clone, PartialEq, Eq)]
2455pub enum ExpiredTimer {
2456 Lock(ExpiredLock),
2457 Delay(ExpiredDelay),
2458}
2459
2460#[derive(Debug, Clone, PartialEq, Eq)]
2461pub struct ExpiredLock {
2462 pub execution_id: ExecutionId,
2463 pub locked_at_version: Version,
2465 pub next_version: Version,
2466 pub intermittent_event_count: u32,
2468 pub max_retries: Option<u32>,
2469 pub retry_exp_backoff: Duration,
2470 pub locked_by: LockedBy,
2471}
2472
2473#[derive(Debug, Clone, PartialEq, Eq)]
2474pub struct ExpiredDelay {
2475 pub execution_id: ExecutionId,
2476 pub join_set_id: JoinSetId,
2477 pub delay_id: DelayId,
2478}
2479
2480#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2481#[serde(tag = "status", rename_all = "snake_case")]
2482pub enum PendingState {
2483 Locked(PendingStateLocked),
2485
2486 #[display("PendingAt(`{_0}`)")]
2487 PendingAt(PendingStatePendingAt),
2488
2489 #[display("BlockedByJoinSet({_0})")]
2491 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2492
2493 #[display("Paused({_0})")]
2501 Paused(PendingStatePaused),
2502
2503 #[display("Cancelling({_0})")]
2506 Cancelling(PendingStateCancelling),
2507
2508 #[display("Finished: {_0}")]
2509 Finished(PendingStateFinished),
2510}
2511
2512pub enum PendingStateMerged {
2515 Locked {
2516 state: PendingStateLocked,
2517 lifecycle: Lifecycle,
2518 },
2519 PendingAt {
2520 state: PendingStatePendingAt,
2521 lifecycle: Lifecycle,
2522 },
2523 BlockedByJoinSet {
2524 state: PendingStateBlockedByJoinSet,
2525 lifecycle: Lifecycle,
2526 },
2527 Finished(PendingStateFinished),
2528}
2529impl From<PendingState> for PendingStateMerged {
2530 fn from(state: PendingState) -> Self {
2531 match state {
2532 PendingState::Locked(s) => PendingStateMerged::Locked {
2533 state: s,
2534 lifecycle: Lifecycle::Active,
2535 },
2536
2537 PendingState::PendingAt(s) => PendingStateMerged::PendingAt {
2538 state: s,
2539 lifecycle: Lifecycle::Active,
2540 },
2541
2542 PendingState::BlockedByJoinSet(s) => PendingStateMerged::BlockedByJoinSet {
2543 state: s,
2544 lifecycle: Lifecycle::Active,
2545 },
2546
2547 PendingState::Paused(inner) => match inner {
2548 PendingStatePaused::PendingAt(s) => PendingStateMerged::PendingAt {
2549 state: s,
2550 lifecycle: Lifecycle::Paused,
2551 },
2552 PendingStatePaused::BlockedByJoinSet(s) => PendingStateMerged::BlockedByJoinSet {
2553 state: s,
2554 lifecycle: Lifecycle::Paused,
2555 },
2556 },
2557
2558 PendingState::Cancelling(inner) => match inner {
2559 PendingStateCancelling::Locked(s) => PendingStateMerged::Locked {
2560 state: s,
2561 lifecycle: Lifecycle::Cancelling,
2562 },
2563 PendingStateCancelling::PendingAt(s) => PendingStateMerged::PendingAt {
2564 state: s,
2565 lifecycle: Lifecycle::Cancelling,
2566 },
2567 PendingStateCancelling::BlockedByJoinSet(s) => {
2568 PendingStateMerged::BlockedByJoinSet {
2569 state: s,
2570 lifecycle: Lifecycle::Cancelling,
2571 }
2572 }
2573 },
2574
2575 PendingState::Finished(s) => PendingStateMerged::Finished(s),
2576 }
2577 }
2578}
2579
2580#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2581#[display("Locked(`{lock_expires_at}`, {}, {})", locked_by.executor_id, locked_by.run_id)]
2582pub struct PendingStateLocked {
2583 pub locked_by: LockedBy,
2584 pub lock_expires_at: DateTime<Utc>,
2585}
2586
2587#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2588#[display("`{scheduled_at}`, last_lock={last_lock:?}")]
2589pub struct PendingStatePendingAt {
2590 pub scheduled_at: DateTime<Utc>,
2591 pub last_lock: Option<LockedBy>,
2593}
2594
2595#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2596#[display("{join_set_id}, `{lock_expires_at}`, closing={closing}")]
2597pub struct PendingStateBlockedByJoinSet {
2598 pub join_set_id: JoinSetId,
2599 pub lock_expires_at: DateTime<Utc>,
2601 pub closing: bool,
2603}
2604
2605#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2610pub enum PendingStatePaused {
2611 #[display("PendingAt({_0})")]
2612 PendingAt(PendingStatePendingAt),
2613 #[display("BlockedByJoinSet({_0})")]
2614 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2615}
2616
2617#[derive(Debug, Clone, derive_more::Display, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2624pub enum PendingStateCancelling {
2625 #[display("Locked({_0})")]
2626 Locked(PendingStateLocked),
2627 #[display("PendingAt({_0})")]
2628 PendingAt(PendingStatePendingAt),
2629 #[display("BlockedByJoinSet({_0})")]
2630 BlockedByJoinSet(PendingStateBlockedByJoinSet),
2631}
2632
2633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2634pub struct LockedBy {
2635 pub executor_id: ExecutorId,
2636 pub run_id: RunId,
2637}
2638impl From<&Locked> for LockedBy {
2639 fn from(value: &Locked) -> Self {
2640 LockedBy {
2641 executor_id: value.executor_id,
2642 run_id: value.run_id,
2643 }
2644 }
2645}
2646
2647#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
2648#[cfg_attr(any(test, feature = "test"), derive(Deserialize))]
2649pub struct PendingStateFinished {
2650 pub version: VersionType, pub finished_at: DateTime<Utc>,
2652 pub result_kind: PendingStateFinishedResultKind,
2653}
2654impl Display for PendingStateFinished {
2655 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2656 match self.result_kind {
2657 PendingStateFinishedResultKind::Ok => write!(f, "OK"),
2658 PendingStateFinishedResultKind::Err(err) => write!(f, "{err}"),
2659 }
2660 }
2661}
2662
2663#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2665#[serde(rename_all = "snake_case")]
2666pub enum PendingStateFinishedResultKind {
2667 Ok,
2668 Err(PendingStateFinishedError),
2669}
2670impl PendingStateFinishedResultKind {
2671 pub fn as_result(&self) -> Result<(), &PendingStateFinishedError> {
2672 match self {
2673 PendingStateFinishedResultKind::Ok => Ok(()),
2674 PendingStateFinishedResultKind::Err(err) => Err(err),
2675 }
2676 }
2677}
2678
2679impl From<&SupportedFunctionReturnValue> for PendingStateFinishedResultKind {
2680 fn from(result: &SupportedFunctionReturnValue) -> Self {
2681 result.as_pending_state_finished_result()
2682 }
2683}
2684
2685#[derive(
2686 Debug,
2687 Clone,
2688 Copy,
2689 PartialEq,
2690 Eq,
2691 Serialize,
2692 Deserialize,
2693 derive_more::Display,
2694 schemars::JsonSchema,
2695)]
2696#[serde(rename_all = "snake_case")]
2697pub enum PendingStateFinishedError {
2698 #[display("Execution failure ({_0})")]
2699 ExecutionFailure(ExecutionFailureKind),
2700 #[display("Error")]
2701 Error,
2702}
2703
2704impl PendingState {
2705 #[instrument(skip(self))]
2706 pub fn can_append_lock(
2707 &self,
2708 created_at: DateTime<Utc>,
2709 executor_id: ExecutorId,
2710 run_id: RunId,
2711 lock_expires_at: DateTime<Utc>,
2712 ) -> Result<LockKind, DbErrorWriteNonRetriable> {
2713 if lock_expires_at <= created_at {
2714 return Err(DbErrorWriteNonRetriable::ValidationFailed(
2715 "invalid expiry date".into(),
2716 ));
2717 }
2718 match self {
2719 PendingState::PendingAt(PendingStatePendingAt {
2720 scheduled_at,
2721 last_lock,
2722 }) => {
2723 if *scheduled_at <= created_at {
2724 Ok(LockKind::CreatingNewLock)
2726 } else if let Some(LockedBy {
2727 executor_id: last_executor_id,
2728 run_id: last_run_id,
2729 }) = last_lock
2730 && executor_id == *last_executor_id
2731 && run_id == *last_run_id
2732 {
2733 Ok(LockKind::Extending)
2735 } else {
2736 Err(DbErrorWriteNonRetriable::ValidationFailed(
2737 "cannot lock, not yet pending".into(),
2738 ))
2739 }
2740 }
2741 PendingState::Locked(PendingStateLocked {
2742 locked_by:
2743 LockedBy {
2744 executor_id: current_pending_state_executor_id,
2745 run_id: current_pending_state_run_id,
2746 },
2747 lock_expires_at: _,
2748 }) => {
2749 if executor_id == *current_pending_state_executor_id
2750 && run_id == *current_pending_state_run_id
2751 {
2752 Ok(LockKind::Extending)
2754 } else {
2755 Err(DbErrorWriteNonRetriable::IllegalState {
2756 reason: "cannot lock, already locked".into(),
2757 context: SpanTrace::capture(),
2758 source: None,
2759 loc: Location::caller(),
2760 })
2761 }
2762 }
2763 PendingState::BlockedByJoinSet { .. } => Err(DbErrorWriteNonRetriable::IllegalState {
2764 reason: "cannot append Locked event when in BlockedByJoinSet state".into(),
2765 context: SpanTrace::capture(),
2766 source: None,
2767 loc: Location::caller(),
2768 }),
2769 PendingState::Finished { .. } => Err(DbErrorWriteNonRetriable::IllegalState {
2770 reason: "already finished".into(),
2771 context: SpanTrace::capture(),
2772 source: None,
2773 loc: Location::caller(),
2774 }),
2775 PendingState::Paused(..) => Err(DbErrorWriteNonRetriable::IllegalState {
2776 reason: "cannot lock, execution is paused".into(),
2777 context: SpanTrace::capture(),
2778 source: None,
2779 loc: Location::caller(),
2780 }),
2781 PendingState::Cancelling(..) => Err(DbErrorWriteNonRetriable::IllegalState {
2782 reason: "cannot lock, execution is cancelling".into(),
2783 context: SpanTrace::capture(),
2784 source: None,
2785 loc: Location::caller(),
2786 }),
2787 }
2788 }
2789
2790 #[must_use]
2791 pub fn is_finished(&self) -> bool {
2792 matches!(self, PendingState::Finished { .. })
2793 }
2794
2795 #[must_use]
2796 pub fn is_paused(&self) -> bool {
2797 matches!(self, PendingState::Paused(_))
2798 }
2799
2800 #[must_use]
2801 pub fn is_cancelling(&self) -> bool {
2802 matches!(self, PendingState::Cancelling(_))
2803 }
2804}
2805
2806#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2807pub enum LockKind {
2808 Extending,
2809 CreatingNewLock,
2810}
2811
2812pub mod http_client_trace {
2813 use chrono::{DateTime, Utc};
2814 use serde::{Deserialize, Serialize};
2815
2816 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2817 pub struct HttpClientTrace {
2818 pub req: RequestTrace,
2819 pub resp: Option<ResponseTrace>,
2820 }
2821
2822 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2823 pub struct RequestTrace {
2824 pub sent_at: DateTime<Utc>,
2825 pub uri: String,
2826 pub method: String,
2827 }
2828
2829 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
2830 pub struct ResponseTrace {
2831 pub finished_at: DateTime<Utc>,
2832 pub status: Result<u16, String>,
2833 }
2834}
2835
2836#[derive(schemars::JsonSchema)]
2838pub struct DbStorageSchema {
2839 pub execution_event: ExecutionEvent,
2840 pub pending_state: PendingState,
2841 pub join_set_response: JoinSetResponse,
2842 pub wasm_backtrace: WasmBacktrace,
2843 pub persisted_function_metadata: PersistedFunctionMetadata,
2844}
2845
2846#[cfg(test)]
2847mod tests {
2848 use super::HistoryEvent;
2849 use super::HistoryEventScheduleAt;
2850 use super::JoinNextTryOutcome;
2851 use super::PendingStateFinished;
2852 use super::PendingStateFinishedError;
2853 use super::PendingStateFinishedResultKind;
2854 use crate::ExecutionFailureKind;
2855 use crate::JoinSetId;
2856 use crate::SupportedFunctionReturnValue;
2857 use chrono::DateTime;
2858 use chrono::Datelike;
2859 use chrono::Utc;
2860 use insta::assert_snapshot;
2861 use rstest::rstest;
2862 use std::time::Duration;
2863 use val_json::type_wrapper::TypeWrapper;
2864 use val_json::wast_val::WastVal;
2865 use val_json::wast_val::WastValWithType;
2866
2867 #[rstest(expected => [
2868 PendingStateFinishedResultKind::Ok,
2869 PendingStateFinishedResultKind::Err(PendingStateFinishedError::ExecutionFailure(ExecutionFailureKind::TimedOut)),
2870 ])]
2871 #[test]
2872 fn serde_pending_state_finished_result_kind_should_work(
2873 expected: PendingStateFinishedResultKind,
2874 ) {
2875 let ser = serde_json::to_string(&expected).unwrap();
2876 let actual: PendingStateFinishedResultKind = serde_json::from_str(&ser).unwrap();
2877 assert_eq!(expected, actual);
2878 }
2879
2880 #[test]
2881 fn result_kind_json_constants_match_serde() {
2882 assert_eq!(
2883 crate::storage::RESULT_KIND_JSON_OK,
2884 serde_json::to_string(&PendingStateFinishedResultKind::Ok).unwrap()
2885 );
2886 assert_eq!(
2887 crate::storage::RESULT_KIND_JSON_ERROR,
2888 serde_json::to_string(&PendingStateFinishedResultKind::Err(
2889 PendingStateFinishedError::Error
2890 ))
2891 .unwrap()
2892 );
2893 }
2894
2895 #[rstest(result_kind => [
2896 PendingStateFinishedResultKind::Ok,
2897 PendingStateFinishedResultKind::Err(PendingStateFinishedError::ExecutionFailure(ExecutionFailureKind::TimedOut)),
2898 ])]
2899 #[test]
2900 fn serde_pending_state_finished_should_work(result_kind: PendingStateFinishedResultKind) {
2901 let expected = PendingStateFinished {
2902 version: 0,
2903 finished_at: Utc::now(),
2904 result_kind,
2905 };
2906
2907 let ser = serde_json::to_string(&expected).unwrap();
2908 let actual: PendingStateFinished = serde_json::from_str(&ser).unwrap();
2909 assert_eq!(expected, actual);
2910 }
2911
2912 #[test]
2913 fn join_set_deser_with_result_ok_option_none_should_work() {
2914 let expected = SupportedFunctionReturnValue::Ok(Some(WastValWithType {
2915 r#type: TypeWrapper::Result {
2916 ok: Some(Box::new(TypeWrapper::Option(Box::new(TypeWrapper::String)))),
2917 err: Some(Box::new(TypeWrapper::String)),
2918 },
2919 value: WastVal::Result(Ok(Some(Box::new(WastVal::Option(None))))),
2920 }));
2921 let json = serde_json::to_string(&expected).unwrap();
2922 assert_snapshot!(json);
2923
2924 let actual: SupportedFunctionReturnValue = serde_json::from_str(&json).unwrap();
2925
2926 assert_eq!(expected, actual);
2927 }
2928
2929 #[test]
2930 fn as_date_time_should_work_with_duration_u32_max_secs() {
2931 let duration = Duration::from_secs(u64::from(u32::MAX));
2932 let schedule_at = HistoryEventScheduleAt::In(duration);
2933 let resolved = schedule_at.as_date_time(DateTime::UNIX_EPOCH).unwrap();
2934 assert_eq!(2106, resolved.year());
2935 }
2936
2937 const MILLIS_PER_SEC: i64 = 1000;
2938 const TIMEDELTA_MAX_SECS: i64 = i64::MAX / MILLIS_PER_SEC;
2939
2940 #[test]
2941 fn as_date_time_should_fail_on_duration_secs_greater_than_i64_max() {
2942 let duration = Duration::from_secs(
2944 u64::try_from(TIMEDELTA_MAX_SECS).expect("positive number must not fail") + 1,
2945 );
2946 let schedule_at = HistoryEventScheduleAt::In(duration);
2947 schedule_at.as_date_time(DateTime::UNIX_EPOCH).unwrap_err();
2948 }
2949
2950 #[test]
2951 fn join_next_try_outcome_new_format() {
2952 let json = r#"{"type":"join_next_try","join_set_id":"n:test","outcome":"found"}"#;
2953 let event: HistoryEvent = serde_json::from_str(json).unwrap();
2954 assert_eq!(
2955 event,
2956 HistoryEvent::JoinNextTry {
2957 join_set_id: JoinSetId::new(
2958 crate::JoinSetKind::Named,
2959 crate::StrVariant::Static("test")
2960 )
2961 .unwrap(),
2962 outcome: JoinNextTryOutcome::Found,
2963 }
2964 );
2965
2966 let json = r#"{"type":"join_next_try","join_set_id":"n:test","outcome":"all_processed"}"#;
2967 let event: HistoryEvent = serde_json::from_str(json).unwrap();
2968 assert_eq!(
2969 event,
2970 HistoryEvent::JoinNextTry {
2971 join_set_id: JoinSetId::new(
2972 crate::JoinSetKind::Named,
2973 crate::StrVariant::Static("test")
2974 )
2975 .unwrap(),
2976 outcome: JoinNextTryOutcome::AllProcessed,
2977 }
2978 );
2979 }
2980
2981 #[test]
2982 fn join_next_try_outcome_serializes_new_format() {
2983 let event = HistoryEvent::JoinNextTry {
2984 join_set_id: JoinSetId::new(
2985 crate::JoinSetKind::Named,
2986 crate::StrVariant::Static("test"),
2987 )
2988 .unwrap(),
2989 outcome: JoinNextTryOutcome::AllProcessed,
2990 };
2991 let json = serde_json::to_string(&event).unwrap();
2992 assert!(
2993 json.contains(r#""outcome":"all_processed""#),
2994 "expected outcome field, got: {json}"
2995 );
2996 assert!(
2997 !json.contains("found_response"),
2998 "should not contain old field, got: {json}"
2999 );
3000 }
3001
3002 mod stub_retval_hash {
3003 use super::super::{StubRetVal, StubRetValHash};
3004 use crate::SupportedFunctionReturnValue;
3005 use val_json::type_wrapper::TypeWrapper;
3006 use val_json::wast_val::{WastVal, WastValWithType};
3007
3008 #[test]
3009 fn typed_variant_hash_is_stable() {
3010 let retval =
3011 StubRetVal::Typed(SupportedFunctionReturnValue::Ok(Some(WastValWithType {
3012 r#type: TypeWrapper::String,
3013 value: WastVal::String("hello".into()),
3014 })));
3015 let hash = retval.hash();
3016 assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
3018 assert_eq!(hash.to_string().len(), 66);
3020 }
3021
3022 #[test]
3023 fn untyped_variant_hash_is_stable() {
3024 let retval = StubRetVal::Untyped(r#"{"ok": "hello"}"#.to_string());
3025 let hash = retval.hash();
3026 assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
3028 assert_eq!(hash.to_string().len(), 66);
3030 }
3031
3032 #[test]
3033 fn different_values_produce_different_hashes() {
3034 let typed1 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3035 let typed2 = StubRetVal::Typed(SupportedFunctionReturnValue::Err(None));
3036 let untyped1 = StubRetVal::Untyped("value1".to_string());
3037 let untyped2 = StubRetVal::Untyped("value2".to_string());
3038
3039 let hashes: Vec<_> = [typed1, typed2, untyped1, untyped2]
3040 .into_iter()
3041 .map(|r| r.hash().to_string())
3042 .collect();
3043
3044 for (i, h1) in hashes.iter().enumerate() {
3046 for h2 in hashes.iter().skip(i + 1) {
3047 assert_ne!(h1, h2, "hashes should be different");
3048 }
3049 }
3050 }
3051
3052 #[test]
3053 fn same_values_produce_same_hashes() {
3054 let retval1 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3055 let retval2 = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3056 assert_eq!(retval1.hash(), retval2.hash());
3057
3058 let untyped1 = StubRetVal::Untyped("test".to_string());
3059 let untyped2 = StubRetVal::Untyped("test".to_string());
3060 assert_eq!(untyped1.hash(), untyped2.hash());
3061 }
3062
3063 #[test]
3064 fn hash_serialization_roundtrip() {
3065 let retval = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3066 let hash = retval.hash();
3067
3068 let serialized = serde_json::to_string(&hash).unwrap();
3069 let deserialized: StubRetValHash = serde_json::from_str(&serialized).unwrap();
3070
3071 assert_eq!(hash, deserialized);
3072 }
3073
3074 #[test]
3075 fn hash_display_and_fromstr_roundtrip() {
3076 let retval = StubRetVal::Untyped("test value".to_string());
3077 let hash = retval.hash();
3078
3079 let display = hash.to_string();
3080 let parsed: StubRetValHash = display.parse().unwrap();
3081
3082 assert_eq!(hash, parsed);
3083 }
3084
3085 #[test]
3086 fn typed_and_untyped_with_same_content_produce_different_hashes() {
3087 let typed = StubRetVal::Typed(SupportedFunctionReturnValue::Ok(None));
3089 let json_of_typed =
3090 serde_json::to_string(&SupportedFunctionReturnValue::Ok(None)).unwrap();
3091 let untyped = StubRetVal::Untyped(json_of_typed);
3092
3093 assert_ne!(typed.hash(), untyped.hash());
3094 }
3095 }
3096}