Skip to main content

obeli_sk_concepts/
storage.rs

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