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