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