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