Skip to main content

obeli_sk_concepts/
storage.rs

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