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