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