Skip to main content

obeli_sk_concepts/
storage.rs

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
37// Shared between databases. TODO: Extract to db-common
38pub 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";
42// `lifecycle` column values on `t_state`: an override of the underlying pending
43// state. Mutually exclusive by construction (single column).
44pub const LIFECYCLE_ACTIVE: &str = "active";
45pub const LIFECYCLE_PAUSED: &str = "paused";
46pub const LIFECYCLE_CANCELLING: &str = "cancelling";
47
48/// Typed view of the `t_state.lifecycle` column. Mutually exclusive by
49/// construction (single column), so pause and cancellation cannot coexist.
50#[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}
75// JSON encodings of `PendingStateFinishedResultKind` as stored in the `result_kind` column,
76// pinned by `result_kind_json_constants_match_serde`.
77pub 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"; // Serialization tag of `HistoryEvent::JoinNext`
80
81#[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, // Is not advanced once in Finished state
87    pub pending_state: PendingState, // reflecting the current state
88    pub component_digest: ComponentDigest, // reflecting the current state
89    pub component_type: ComponentType,
90    pub deployment_id: DeploymentId, // reflecting the current state
91}
92
93impl ExecutionLog {
94    /// Return some duration after which the execution will be retried.
95    /// Return `None` if no more retries are allowed.
96    #[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 max_retries == None, wrapping is OK after this succeeds - we want to retry forever.
103        if temporary_event_count <= max_retries.unwrap_or(u32::MAX) {
104            // TODO: Add test for number of retries
105            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, /* webapi */
294    schemars::JsonSchema,
295)]
296pub struct ResponseCursor(pub u32);
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize /* webapi */, 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 /* webapi */, 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}")] // execution completed..
343    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    /// Releases a lock.
401    ///
402    /// State transition semantics:
403    /// - [`PendingState::Locked`] becomes [`PendingState::PendingAt`] at
404    ///   [`Unlocked::backoff_expires_at`]. The field name is kept for persisted JSON and gRPC
405    ///   compatibility, but it is the next pending instant for every unlock reason.
406    /// - [`PendingState::PendingAt`], [`PendingState::BlockedByJoinSet`],
407    ///   [`PendingState::Paused`], and [`PendingState::Finished`] reject this event.
408    #[display("Unlocked({_0})")]
409    Unlocked(Unlocked),
410    /// Does not change `PendingState`.
411    #[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    // Created by the executor holding the lock.
420    // After expiry interpreted as pending.
421    #[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    // Created by the executor holding the lock.
431    // After expiry interpreted as pending.
432    #[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    // Created by the executor holding the lock.
439    #[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    /// Requests cancellation. Sets `lifecycle` to `cancelling` without changing
456    /// the underlying state; the cancellation driver or activity owner then
457    /// appends `Finished(Cancelled)`.
458    ///
459    /// State transition semantics (mirrors [`ExecutionRequest::Paused`]):
460    /// - Every non-terminal state sets `lifecycle = cancelling`, underlying state
461    ///   unchanged. A paused execution is never running; a locked activity keeps
462    ///   its lock until teardown or lease expiry; a locked workflow's run is
463    ///   fenced by this event's version bump.
464    /// - [`PendingState::Finished`] is rejected (already terminal).
465    #[display("CancellationRequested")]
466    CancellationRequested,
467}
468
469/// Reason for auditing only
470#[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    /// Instant used when releasing a currently locked execution back to [`PendingState::PendingAt`].
489    // The field was renamed only in Rust; keep its serialized name stable.
490    #[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    /// String representation of `ExecutionRequest`, used in execution log table to fetch events of certain type, e.g. `created` + `history_event`.
521    #[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")]
623/// Must be created by the executor in [`PendingState::Locked`].
624pub enum HistoryEvent {
625    /// Persist a generated pseudorandom value.
626    #[display("Persist")]
627    Persist {
628        #[debug(skip)]
629        value: Vec<u8>, // Only stored for nondeterminism checks. TODO: Consider using a hashed value or just the intention.
630        kind: PersistKind,
631    },
632    #[display("JoinSetCreate({join_set_id})")]
633    JoinSetCreate { join_set_id: JoinSetId },
634    #[display("JoinSetRequest({request})")]
635    // join_set_id is part of ExecutionId or DelayId in the `request`
636    JoinSetRequest {
637        join_set_id: JoinSetId,
638        request: JoinSetRequest,
639    },
640    /// Sets the pending state to [`PendingState::BlockedByJoinSet`].
641    /// When the response arrives at `resp_time`:
642    /// The execution is [`PendingState::PendingAt`]`(max(resp_time, lock_expires_at)`, so that the
643    /// original executor can continue. After the expiry any executor can continue without
644    /// marking the execution as timed out.
645    #[display("JoinNext({join_set_id})")]
646    JoinNext {
647        join_set_id: JoinSetId,
648        /// Set to a future time if the worker is keeping the execution invocation warm waiting for the result.
649        /// The pending status will be kept in Locked state until `run_expires_at`.
650        run_expires_at: DateTime<Utc>,
651        /// Set to a specific function when calling `-await-next` extension function, used for
652        /// determinism checks.
653        requested_ffqn: Option<FunctionFqn>,
654        /// Closing request must never set `requested_ffqn` and is ignored by determinism checks.
655        closing: bool,
656    },
657    /// Attempt to process next response without changing the pending state.
658    #[display("JoinNextTry({join_set_id}, {outcome})")]
659    JoinNextTry {
660        join_set_id: JoinSetId,
661        outcome: JoinNextTryOutcome,
662    },
663    /// Records the fact that a join set was awaited more times than its submission count.
664    #[display("JoinNextTooMany({join_set_id})")]
665    JoinNextTooMany {
666        join_set_id: JoinSetId,
667        /// Set to a specific function when calling `-await-next` extension function, used for
668        /// determinism checks.
669        requested_ffqn: Option<FunctionFqn>,
670    },
671    #[display("Schedule({execution_id}, {schedule_at})")]
672    Schedule {
673        execution_id: ExecutionId,
674        schedule_at: HistoryEventScheduleAt, // Stores intention to schedule an execution at a date/offset
675        #[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/// Stub return value - only used during processing, not stored in history.
689#[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    /// Compute a stable hash of the return value for determinism checks.
699    #[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                // Serialize to JSON for stable hashing
709                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/// Hash of a stub return value, stored in history for determinism checks.
729/// Format: 1 byte version + 32 bytes SHA-256 hash.
730#[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            // 33 bytes * 2 hex chars = 66
762            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/// Error from the `-stub` extension function.
783/// Mirrors `obelisk:types/execution.{stub-error}` from WIT.
784#[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/// Error from the `schedule-json` function. Persisted in history for determinism.
798#[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/// Error from the `submit-json` function. Persisted in history for determinism.
810#[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    /// A response was found and processed.
836    #[display("found")]
837    Found,
838    /// No response available, but there are still pending requests.
839    #[display("pending")]
840    Pending,
841    /// No response available, and all requests have been processed.
842    #[display("all_processed")]
843    AllProcessed,
844}
845
846impl From<bool> for JoinNextTryOutcome {
847    /// Migration helper: converts old `found_response: bool` to the new enum.
848    /// `false` maps to `Pending` as a conservative default (the exact error
849    /// was not stored before).
850    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    // Must be created by the executor in `PendingState::Locked`.
911    #[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    // Must be created by the executor in `PendingState::Locked`.
920    #[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/// Error that is not specific to an execution.
932#[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/// Write error tied to an execution
980#[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/// Error from idempotent stub response write.
991#[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/// Read error tied to an execution
1000#[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
1032// Represents next version after successfuly appended to execution log.
1033// TODO: Convert to struct with next_version
1034pub 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    /// Content-addressed blob store for deployment files. Separate from the metadata
1102    /// connections so the bytes can move to an object store (S3) in future while the
1103    /// referencing metadata stays in the database.
1104    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/// A captured database write operation with all arguments needed to replay it
1135/// against the real database.
1136/// Dates carry meaning only on a fresh replay, ignoring user's input when persisting.
1137#[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    /// Append a single event to an existing execution log.
1253    /// The request cannot contain [`ExecutionRequest::Created`].
1254    async fn append(
1255        &self,
1256        execution_id: ExecutionId,
1257        version: Version,
1258        req: AppendRequest,
1259    ) -> Result<AppendResponse, DbErrorWrite>;
1260
1261    /// Append a batch of events to an existing execution log, and append a response to a parent execution.
1262    /// The batch cannot contain [`ExecutionRequest::Created`].
1263    async fn append_batch_respond_to_parent(
1264        &self,
1265        events: AppendEventsToExecution,
1266        response: AppendResponseToExecution,
1267        current_time: DateTime<Utc>, // not persisted, can be used for unblocking `subscribe_to_pending`
1268    ) -> Result<AppendBatchResponse, DbErrorWrite>;
1269
1270    /// Notification mechainism with no strict guarantees for waiting while there are no pending executions.
1271    /// Return immediately if there are pending notifications at `pending_at_or_sooner`.
1272    /// Otherwise wait until `timeout_fut` resolves.
1273    /// Delay requests that expire between `pending_at_or_sooner` and timeout can be disregarded.
1274    /// If `current_digest` is set, ignore executions with incompatible digests.
1275    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    /// Notification mechainism with no strict guarantees for waiting while there are no pending executions.
1284    /// Return immediately if there are pending notifications at `pending_at_or_sooner`.
1285    /// Otherwise wait until `timeout_fut` resolves.
1286    /// Delay requests that expire between `pending_at_or_sooner` and timeout can be disregarded.
1287    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    /// See [`Self::append_activity_cancellation_requested`].
1295    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    /// Request cancellation of a cancellable workflow. In one version-guarded
1315    /// transaction, appends [`ExecutionRequest::CancellationRequested`]; rejects a non-cancellable
1316    /// target and returns `AlreadyFinished`/`AlreadyCancelling` without appending.
1317    /// The `Finished(Cancelled)` outcome is driven later by the cancellation driver.
1318    async fn cancel_workflow(
1319        &self,
1320        execution_id: &ExecutionId,
1321        cancelled_at: DateTime<Utc>,
1322    ) -> Result<CancelOutcome, DbErrorWrite>;
1323
1324    /// Request cancellation of a cancellable workflow, retrying the version-guarded
1325    /// transaction on the live-worker race.
1326    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    /// Get last event. Impls may set `ExecutionEvent::backtrace_id` to `None`.
1343    async fn get_last_execution_event(
1344        &self,
1345        execution_id: &ExecutionId,
1346    ) -> Result<ExecutionEvent, DbErrorRead>;
1347
1348    /// Append [`ExecutionRequest::CancellationRequested`] if execution is not finished and not in
1349    /// cancellation already.
1350    /// The state will become [`PendingStateCancelling`] with the underlying state embedded.
1351    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    /// Match executions in any of the given states (logical OR). Empty list matches all.
1373    /// All [`ExecutionStateFilter::Pending`] / [`ExecutionStateFilter::Scheduled`] entries
1374    /// must carry the same `now`.
1375    pub state_filters: Vec<ExecutionStateFilter>,
1376}
1377
1378/// Filter executions by their current state, using the same buckets as the deployment summary.
1379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1380pub enum ExecutionStateFilter {
1381    Locked,
1382    /// [`PendingState::PendingAt`] with the scheduled time at or before `now`.
1383    Pending {
1384        now: DateTime<Utc>,
1385    },
1386    /// [`PendingState::PendingAt`] with the scheduled time after `now`.
1387    Scheduled {
1388        now: DateTime<Utc>,
1389    },
1390    Blocked,
1391    /// Paused regardless of the underlying state (locked, pending or blocked).
1392    Paused,
1393    /// Cancellation requested; teardown in progress (underlying state pending or blocked).
1394    Cancelling,
1395    /// Finished with any result.
1396    Finished,
1397    /// Finished successfully.
1398    FinishedOk,
1399    /// Finished with the `err` variant of the result type.
1400    FinishedError,
1401    /// Execution failure: trap, timeout, nondeterminism, cancellation etc.
1402    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    /// Get the latest backtrace if version is not set.
1436    async fn get_backtrace(
1437        &self,
1438        execution_id: &ExecutionId,
1439        filter: BacktraceFilter,
1440    ) -> Result<BacktraceInfo, DbErrorRead>;
1441
1442    /// Map a backtrace source file (a blob already stored in the CAS) to a component digest.
1443    /// `frame_key` is either an exact frame symbol path or a suffix (with leading `/`)
1444    /// when `is_suffix` is true. Repeated calls replace the digest mapped to the same key.
1445    /// The blob bytes themselves live in the CAS (see [`crate::cas::Cas`]); only this mapping
1446    /// lives in the database.
1447    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    /// Resolve a backtrace source file's CAS digest by component digest and a frame symbol path.
1456    /// Matches either exact keys or suffix keys (where the frame path ends with the stored key).
1457    /// Returns `None` if not found or if multiple suffix entries match (ambiguous). The caller
1458    /// fetches the bytes from the CAS (see [`crate::cas::Cas`]).
1459    async fn resolve_source_digest(
1460        &self,
1461        component_digest: &ComponentDigest,
1462        file: &str,
1463    ) -> Result<Option<ContentDigest>, DbErrorRead>;
1464
1465    /// Insert or reuse normalized component metadata rows.
1466    async fn upsert_component_metadata(
1467        &self,
1468        records: Vec<ComponentMetadataRecord>,
1469    ) -> Result<(), DbErrorWrite>;
1470
1471    /// Insert deployment-local component bindings for a deployment.
1472    async fn insert_deployment_components(
1473        &self,
1474        deployment_id: DeploymentId,
1475        records: Vec<DeploymentComponentRecord>,
1476    ) -> Result<(), DbErrorWrite>;
1477
1478    /// List all components visible in a deployment, including persisted imports, exports and WIT.
1479    async fn list_deployment_components(
1480        &self,
1481        deployment_id: DeploymentId,
1482    ) -> Result<Vec<DeploymentComponentDetail>, DbErrorRead>;
1483
1484    /// Get the WIT for a component digest scoped to a deployment.
1485    async fn get_deployment_component_wit(
1486        &self,
1487        deployment_id: DeploymentId,
1488        component_digest: &ComponentDigest,
1489    ) -> Result<Option<String>, DbErrorRead>;
1490
1491    /// Returns executions sorted in descending order.
1492    async fn list_executions(
1493        &self,
1494        filter: ListExecutionsFilter,
1495        pagination: ExecutionListPagination,
1496    ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric>;
1497
1498    /// Returns execution events for the given execution.
1499    ///
1500    /// Results are always ordered from oldest to newest (ascending by version),
1501    /// regardless of pagination direction.
1502    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    /// Returns responses of an execution ordered as they arrived,
1510    /// enabling matching each `JoinNext` to its corresponding response.
1511    ///
1512    /// Results are always ordered from oldest to newest (ascending by cursor),
1513    /// regardless of pagination direction.
1514    ///
1515    /// As an optimization, the implementation can return an empty list of `responses`
1516    /// and `max_cursor` set to 0 if the execution is not found.
1517    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    /// Insert a new deployment. The record must have `status == Inactive` and
1567    /// `last_active_at == None`; activation is a separate step via [`Self::activate_deployment`].
1568    async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite>;
1569
1570    /// [`Self::insert_deployment`] + [`Self::upsert_component_metadata`] +
1571    /// [`Self::insert_deployment_components`] in a single transaction, so the deployment row
1572    /// and its component rows commit together or not at all. Same preconditions as
1573    /// [`Self::insert_deployment`].
1574    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    /// Return deployment file digests referenced by this deployment but absent from the CAS.
1583    ///
1584    /// Blob bytes themselves are stored and fetched through the separate [`crate::cas::Cas`]
1585    /// trait (see [`DbPool::cas_conn`]); only this metadata/completeness query lives on the `Db`.
1586    async fn missing_digests(
1587        &self,
1588        deployment_id: DeploymentId,
1589    ) -> Result<Vec<ContentDigest>, DbErrorRead>;
1590
1591    /// Return the deployment-owned file refs recorded for a deployment.
1592    async fn list_deployment_files(
1593        &self,
1594        deployment_id: DeploymentId,
1595    ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead>;
1596
1597    /// Delete content-addressed file blobs not referenced by any stored deployment,
1598    /// returning the number deleted. Such orphans are left behind when a submit writes
1599    /// blobs to the store and then fails verification before persisting the deployment.
1600    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    /// Mark a deployment as Enqueued (pending next server restart).
1609    /// Any previously Enqueued deployment is demoted to Inactive. If the target deployment is
1610    /// currently Active, it remains Active and any previously Enqueued deployment is cleared.
1611    /// The returned [`EnqueueOutcome`] reflects which of those happened.
1612    async fn enqueue_deployment(
1613        &self,
1614        deployment_id: DeploymentId,
1615    ) -> Result<EnqueueOutcome, DbErrorWrite>;
1616
1617    /// Returned [`DeploymentRecord`] must contain `deployment_toml`.
1618    async fn get_deployment(
1619        &self,
1620        deployment_id: DeploymentId,
1621    ) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1622
1623    /// Return active deployment.
1624    /// Returned [`DeploymentRecord`] must contain `deployment_toml`.
1625    #[cfg(feature = "test")]
1626    async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead>;
1627
1628    /// Return the most relevant current deployment: Enqueued if present, otherwise Active.
1629    /// Returned [`DeploymentRecord`] must contain `deployment_toml`.
1630    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    /// Pause an execution.
1638    /// If the execution is an activity and is currently in `PendingState::Locked`, implementations must
1639    /// reject the write, otherwise a running activity will be considered terminated, which can break
1640    /// structured concurrency guarantees.
1641    async fn pause_execution(
1642        &self,
1643        execution_id: &ExecutionId,
1644        paused_at: DateTime<Utc>,
1645    ) -> Result<AppendResponse, DbErrorWrite>;
1646
1647    /// Unpause an execution. Only paused executions can be unpaused.
1648    async fn unpause_execution(
1649        &self,
1650        execution_id: &ExecutionId,
1651        unpaused_at: DateTime<Utc>,
1652    ) -> Result<AppendResponse, DbErrorWrite>;
1653
1654    /// Pause a delay, preventing it from being picked up by the expired timers watcher.
1655    /// No-op if the delay is already paused.
1656    /// Returns `NotFound` if the delay does not exist (already processed or cancelled).
1657    async fn pause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite>;
1658
1659    /// Unpause a previously paused delay.
1660    /// No-op if the delay is already unpaused.
1661    /// Returns `NotFound` if the delay does not exist (already processed or cancelled).
1662    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    /// Content digest = `sha256(deployment_toml)`.
1676    pub digest: ContentDigest,
1677    pub locked: u32,
1678    // In `PendingAt` state, scheduled to present or past
1679    pub pending: u32,
1680    // In `PendingAt` state, scheduled into the future
1681    pub scheduled: u32,
1682    pub blocked: u32,
1683    // Paused regardless of the underlying state (locked, pending or blocked).
1684    pub paused: u32,
1685    // Cancellation requested; teardown in progress. Disjoint from the buckets above.
1686    pub cancelling: u32,
1687    pub finished_ok: u32,
1688    pub finished_error: u32,
1689    pub finished_execution_failure: u32,
1690    /// Verbatim deployment manifest. None if not requested from db.
1691    pub deployment_toml: Option<String>,
1692    pub created_at: DateTime<Utc>,
1693    /// Set when the deployment becomes Active; None if it has never been active.
1694    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 the aggregate queries; every bucket is reported as zero.
1701    Skip,
1702    /// Count executions per bucket; `include_derived` also counts child executions.
1703    Count { include_derived: bool },
1704}
1705
1706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1707pub enum DeploymentStatus {
1708    Inactive,
1709    /// Queued to become Active on the next server restart.
1710    Enqueued,
1711    Active,
1712}
1713
1714/// Outcome of [`DbExternalApi::enqueue_deployment`], reflecting what the transaction did.
1715#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1716pub enum EnqueueOutcome {
1717    /// The target was inactive and is now Enqueued for the next restart.
1718    Enqueued,
1719    /// The target was already Active; it stays Active and any previously Enqueued
1720    /// deployment was cleared.
1721    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    /// Content digest = `sha256(deployment_toml)`.
1752    pub digest: ContentDigest,
1753    pub created_at: DateTime<Utc>,
1754    /// Set when the deployment becomes Active; None if it has never been active.
1755    pub last_active_at: Option<DateTime<Utc>>,
1756    pub status: DeploymentStatus,
1757    pub deployment_toml: String, // `deployment.toml` manifest that client enriched with generated metadata like `content_digest`, see `prepare_deployment_manifest`.
1758    pub obelisk_version: String,
1759    pub created_by: Option<String>,
1760    pub files: Vec<DeploymentFileRecord>,
1761}
1762
1763impl DeploymentRecord {
1764    /// Computes the deployment content digest = `sha256(deployment_toml)`.
1765    #[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/// Origin of a component's WIT: parsed from WASM or synthesized from type wrappers.
1816#[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/// Relation between a deployment and its components
1838#[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>, // Newer logs can always arrive e.g. via replay
1898    pub prev_page: Option<Pagination<LogCursor>>, // None if we are already at the beginning
1899}
1900
1901#[derive(Debug)]
1902pub struct LogFilter {
1903    show_logs: bool,
1904    show_streams: bool,
1905    levels: Vec<LogLevel>, // Only applied if `show_logs` = true, empty means return all levels.
1906    stream_types: Vec<LogStreamType>, // Only applied if `show_streams` = true, empty means return all stream types.
1907    created_after: Option<DateTime<Utc>>,
1908    created_before: Option<DateTime<Utc>>,
1909}
1910impl LogFilter {
1911    // Constructor for logs only
1912    #[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    // Constructor for streams only
1924    #[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    // Constructor for both logs and streams
1936    #[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    // Getters
1948    #[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    /// Get execution log.
1996    async fn get(&self, execution_id: &ExecutionId) -> Result<ExecutionLog, DbErrorRead>;
1997
1998    /// Execution ids whose `lifecycle` is `cancelling`, for the cancellation driver
1999    /// to advance. Ordered oldest-first, capped at `batch_size`. Unlike the executor
2000    /// pick-up queries this is not lock/pause guarded: cancellation proceeds
2001    /// regardless (cancel supersedes pause).
2002    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<(), ()>, // Successfully finished - `Ok(())` or cancelled - `Err(())`
2011    ) -> Result<AppendDelayResponseOutcome, DbErrorWrite>;
2012
2013    /// Append a batch of events to an existing execution log.
2014    /// The batch must not contain [`ExecutionRequest::Created`].
2015    async fn append_batch(
2016        &self,
2017        current_time: DateTime<Utc>, // not persisted, can be used for unblocking `subscribe_to_pending`
2018        batch: Vec<AppendRequest>,
2019        execution_id: ExecutionId,
2020        version: Version,
2021    ) -> Result<AppendBatchResponse, DbErrorWrite>;
2022
2023    /// Append a blocking one-off delay batch (`JoinSetCreate`, `DelayRequest`, `JoinNext`)
2024    /// for a delay that is already due (e.g. `sleep(now)`) together with its `DelayFinished`
2025    /// response, in a single transaction. This unblocks the `JoinNext` immediately, so the
2026    /// workflow resumes without a round trip through the expired-timers watcher.
2027    async fn append_batch_with_delay_response(
2028        &self,
2029        current_time: DateTime<Utc>, // not persisted, can be used for unblocking `subscribe_to_pending`
2030        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    /// Append one or more events to the parent execution log, and create zero or more child execution logs.
2038    /// The batch must not contain [`ExecutionRequest::Created`].
2039    async fn append_batch_create_new_execution(
2040        &self,
2041        current_time: DateTime<Utc>, // not persisted, can be used for unblocking `subscribe_to_pending`
2042        batch: Vec<AppendRequest>,   // must not contain [`ExecutionRequest::Created`] events
2043        execution_id: ExecutionId,
2044        version: Version,
2045        child_req: Vec<CreateRequest>,
2046        backtraces: Vec<BacktraceInfo>,
2047    ) -> Result<AppendBatchResponse, DbErrorWrite>;
2048
2049    /// Get a single event specified by version. Impls may set `ExecutionEvent::backtrace_id` to `None`.
2050    async fn get_execution_event(
2051        &self,
2052        execution_id: &ExecutionId,
2053        version: &Version,
2054    ) -> Result<ExecutionEvent, DbErrorRead>;
2055
2056    /// Idempotent stub response write. Appends a Finished event to the child execution
2057    /// and a response to the parent. If the child is already finished with the same retval,
2058    /// succeeds silently. If finished with a different retval, returns [`DbErrorStubResponse::StubConflict`].
2059    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    /// Get currently expired locks and async timers (delay requests)
2116    async fn get_expired_timers(
2117        &self,
2118        at: DateTime<Utc>,
2119    ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric>;
2120
2121    /// Create a new execution log
2122    async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite>;
2123
2124    /// Notification mechainism with no strict guarantees for getting notified when a new response arrives.
2125    /// Parameter `start_idx` must be at most be equal to current size of responses in the execution log.
2126    /// If no response arrives immediately and `subscription_end_fut` resolves,
2127    /// `SubscribeToResponsesError::SubscriptionEnded` is returned.
2128    /// Implementations with no pubsub support should use polling.
2129    /// Callers are expected to call this function in a loop with a reasonable timeout
2130    /// to support less stellar implementations.
2131    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    /// First, attempt to fetch the finished value. If the execution is not finished yet, poll
2139    /// periodically or subscribe to db changes, racing with `timeout_fut`.
2140    /// Notification mechainism with no strict guarantees for getting the finished result.
2141    /// Implementations with no pubsub support should use polling.
2142    /// Callers are expected to call this function in a loop with a reasonable timeout
2143    /// to support less stellar implementations.
2144    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    /// Returns `TimeoutOutcome::Timeout` if not in Finished state.
2162    #[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); // Stub activities have no execution log except Created event.
2275    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(()), // Mark as cancelled.
2319        )
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, // does not matter when `cursor` is not specified
2445        })
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! { // future's liveness: Dropping the loser immediately.
2563            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    // Version of last `Locked` event, used to detect whether the execution made progress.
2581    pub locked_at_version: Version,
2582    pub next_version: Version,
2583    /// As the execution may still be running, this represents the number of intermittent failures + timeouts prior to this execution.
2584    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    /// Caused by [`ExecutionRequest::Locked`].
2601    Locked(PendingStateLocked),
2602
2603    #[display("PendingAt(`{_0}`)")]
2604    PendingAt(PendingStatePendingAt),
2605
2606    /// Caused by [`HistoryEvent::JoinNext`]
2607    #[display("BlockedByJoinSet({_0})")]
2608    BlockedByJoinSet(PendingStateBlockedByJoinSet),
2609
2610    /// Started by appending [`ExecutionRequest::Paused`] and
2611    /// ended with [`ExecutionRequest::Unpaused`] or [`ExecutionRequest::CancellationRequested`].
2612    ///
2613    /// Activity must not be in-flight, cancelling or finished when pausing.
2614    /// Workflow must not be cancelling or finished.
2615    /// Pausing a locked workflow must first append `Unlocked`.
2616    /// The previous pending state is stored for workflow unpause.
2617    #[display("Paused({_0})")]
2618    Paused(PendingStatePaused),
2619
2620    /// Started by appending [`ExecutionRequest::CancellationRequested`] and
2621    /// ended with [`ExecutionRequest::Finished`].
2622    #[display("Cancelling({_0})")]
2623    Cancelling(PendingStateCancelling),
2624
2625    #[display("Finished: {_0}")]
2626    Finished(PendingStateFinished),
2627}
2628
2629/// [`PendingState`] flattened so the underlying runnable state and the
2630/// [`Lifecycle`] overlay are available side by side.
2631pub 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    /// `last_lock` is needed for lock extension.
2709    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    /// See [`HistoryEvent::JoinNext::lock_expires_at`].
2717    pub lock_expires_at: DateTime<Utc>,
2718    /// Blocked by closing of the join set
2719    pub closing: bool,
2720}
2721
2722/// State of execution before it was paused.
2723///
2724/// A paused activity is always `PendingAt`. Pausing a locked workflow must first
2725/// append `Unlocked`, so `Locked` is never wrapped here.
2726#[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/// Underlying state of a cancelling execution.
2735///
2736/// Tracked for the cancellation driver: an activity whose worker fails to confirm
2737/// teardown is pronounced finished once the `Locked` lease expires. The other
2738/// variants are a frozen snapshot from when cancellation was requested (incoming
2739/// responses do not unblock a cancelling execution), kept for observability.
2740#[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, // not Version since it must be Copy
2768    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// This is not a Result so that it can be customized for serialization
2781#[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                    // pending now, ok to lock
2842                    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                    // Original executor is extending the lock.
2851                    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                    // Original executor is extending the lock.
2870                    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/// Root type for DB schema generation - contains all serialized DB types
2954#[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        // Fails on duration -> timedelta conversion, but a smaller duration can fail on datetime + timedelta
3059        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            // Hash should start with version byte 0x01
3133            assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
3134            // Hash should be 66 hex characters (33 bytes * 2)
3135            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            // Hash should start with version byte 0x01
3143            assert_eq!(hash.to_string().chars().take(2).collect::<String>(), "01");
3144            // Hash should be 66 hex characters (33 bytes * 2)
3145            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            // All hashes should be unique
3161            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            // Even if the JSON content is the same, Typed vs Untyped should hash differently
3204            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}