Skip to main content

obeli_sk_executor/
worker.rs

1use async_trait::async_trait;
2use concepts::ExecutionFailureKind;
3use concepts::ExecutionId;
4use concepts::ExecutionMetadata;
5use concepts::FunctionMetadata;
6use concepts::JoinSetId;
7use concepts::TrapKind;
8use concepts::component_id::ComponentDigest;
9use concepts::storage::DbErrorWrite;
10use concepts::storage::HistoryEvent;
11use concepts::storage::Locked;
12use concepts::storage::ResponseWithCursor;
13use concepts::storage::Version;
14use concepts::storage::http_client_trace::HttpClientTrace;
15use concepts::{FinishedExecutionFailure, StrVariant};
16use concepts::{FunctionFqn, ParamsParsingError, ResultParsingError};
17use concepts::{Params, SupportedFunctionReturnValue};
18use tracing::Span;
19
20#[async_trait]
21pub trait Worker: Send + Sync + 'static {
22    async fn run(&self, ctx: WorkerContext) -> WorkerResult;
23
24    // List exported functions without extensions.
25    // Used by executor.
26    fn exported_functions_noext(&self) -> &[FunctionMetadata];
27}
28
29pub type WorkerResult = Result<WorkerResultOk, WorkerError>;
30
31#[derive(Debug, derive_more::Display)]
32pub enum WorkerResultOk {
33    #[display("db updated, not finished")]
34    DbUpdatedByWorkerOrWatcher,
35    /// The execution run has returned a valid `retval`. Activity with retry budget returning `err` will be retried by the executor.
36    #[display("{_0}")]
37    RunFinished(RunFinished),
38}
39
40#[derive(Debug, Clone, Copy, derive_more::Display)]
41pub enum ExecutionYieldReason {
42    #[display("executor closing")]
43    ExecutorClosing,
44    #[display("workflow event limit reached")]
45    WorkflowEventLimitReached,
46}
47
48#[derive(Debug, derive_more::Display)]
49#[display("finished at v{version}")]
50pub struct RunFinished {
51    pub retval: SupportedFunctionReturnValue,
52    pub version: Version,
53    pub http_client_traces: Option<Vec<HttpClientTrace>>,
54}
55
56#[derive(Debug)]
57pub struct WorkerContext {
58    pub execution_id: ExecutionId,
59    pub metadata: ExecutionMetadata,
60    pub component_digest: ComponentDigest,
61    pub ffqn: FunctionFqn,
62    pub params: Params,
63    pub event_history: Vec<(HistoryEvent, Version)>,
64    pub responses: Vec<ResponseWithCursor>,
65    pub parent: Option<(ExecutionId, JoinSetId)>,
66    pub version: Version,
67    pub can_be_retried: bool,
68    pub worker_span: Span,
69    pub locked_event: Locked,
70    pub execution_interrupt_watcher: tokio::sync::watch::Receiver<bool>,
71}
72
73#[derive(Debug, thiserror::Error)]
74pub enum WorkerError {
75    // retriable errors
76    // Used by activity worker
77    #[error("activity {trap_kind}: {reason}")]
78    ActivityTrap {
79        reason: String,
80        trap_kind: TrapKind,
81        detail: Option<String>,
82        version: Version,
83        http_client_traces: Option<Vec<HttpClientTrace>>,
84    },
85    /// Resources are exhausted.
86    /// Executor must mark the execution as Unlocked.
87    /// This event does not increase temporary event count.
88    #[error("limit reached: {reason}")]
89    LimitReached { reason: String, version: Version },
90    // Used by activity or workflow worker, best effort. If this is not persisted, the expired timers watcher will append it.
91    #[error("temporary timeout")]
92    TemporaryTimeout {
93        http_client_traces: Option<Vec<HttpClientTrace>>,
94        version: Version,
95    },
96    #[error("execution yielded: {reason}")]
97    ExecutionYielded {
98        version: Version,
99        reason: ExecutionYieldReason,
100    },
101    #[error(transparent)]
102    DbError(DbErrorWrite),
103    // non-retriable errors
104    #[error("fatal error: {0}")]
105    FatalError(FatalError, Version),
106}
107
108#[derive(Debug, thiserror::Error)]
109pub enum FatalError {
110    // Used by workflow worker
111    #[error("nondeterminism detected")]
112    NondeterminismDetected { detail: String },
113    // Used by activity worker, workflow worker
114    #[error(transparent)]
115    ParamsParsingError(ParamsParsingError),
116    // Used by activity worker, workflow worker
117    #[error("{reason}")]
118    CannotInstantiate {
119        reason: String,
120        detail: Option<String>,
121    },
122    // Used by activity worker, workflow worker
123    #[error(transparent)]
124    ResultParsingError(ResultParsingError),
125    /// Used when workflow cannot call an imported function, either a child execution or a function from workflow-support.
126    #[error("error calling imported function {ffqn} : {reason}")]
127    ImportedFunctionCallError {
128        ffqn: FunctionFqn,
129        reason: StrVariant,
130        detail: Option<String>,
131    },
132    /// Workflow trap if `retry_on_trap` is disabled.
133    #[error("workflow {trap_kind}: {reason}")]
134    WorkflowTrap {
135        reason: String,
136        trap_kind: TrapKind,
137        detail: Option<String>,
138    },
139    #[error("out of fuel: {reason}")]
140    OutOfFuel { reason: String },
141    #[error("constraint violation: {reason}")]
142    ConstraintViolation { reason: StrVariant },
143    // Applies to activities.
144    #[error("cancelled")]
145    Cancelled,
146}
147
148impl From<FatalError> for FinishedExecutionFailure {
149    fn from(err: FatalError) -> Self {
150        let reason_generic = err.to_string(); // Override with err's reason if no information is lost.
151        match err {
152            FatalError::NondeterminismDetected { detail } => FinishedExecutionFailure {
153                reason: None,
154                kind: ExecutionFailureKind::NondeterminismDetected,
155                detail: Some(detail),
156            },
157            FatalError::OutOfFuel { reason } => FinishedExecutionFailure {
158                reason: Some(reason),
159                kind: ExecutionFailureKind::OutOfFuel,
160                detail: None,
161            },
162            FatalError::ParamsParsingError(err) => FinishedExecutionFailure {
163                reason: Some(reason_generic),
164                kind: ExecutionFailureKind::Uncategorized,
165                detail: err.detail(),
166            },
167            FatalError::CannotInstantiate { reason, detail } => FinishedExecutionFailure {
168                reason: Some(reason),
169                kind: ExecutionFailureKind::Uncategorized,
170                detail,
171            },
172            FatalError::ResultParsingError(_) | FatalError::ConstraintViolation { reason: _ } => {
173                FinishedExecutionFailure {
174                    reason: Some(reason_generic),
175                    kind: ExecutionFailureKind::Uncategorized,
176                    detail: None,
177                }
178            }
179            FatalError::ImportedFunctionCallError { detail, .. }
180            | FatalError::WorkflowTrap { detail, .. } => FinishedExecutionFailure {
181                reason: Some(reason_generic),
182                kind: ExecutionFailureKind::Uncategorized,
183                detail,
184            },
185            FatalError::Cancelled => FinishedExecutionFailure {
186                kind: ExecutionFailureKind::Cancelled,
187                reason: None,
188                detail: None,
189            },
190        }
191    }
192}
193
194impl From<&FatalError> for FinishedExecutionFailure {
195    fn from(err: &FatalError) -> Self {
196        let reason_generic = err.to_string(); // Override with err's reason if no information is lost.
197        match err {
198            FatalError::NondeterminismDetected { detail } => FinishedExecutionFailure {
199                reason: None,
200                kind: ExecutionFailureKind::NondeterminismDetected,
201                detail: Some(detail.clone()),
202            },
203            FatalError::OutOfFuel { reason } => FinishedExecutionFailure {
204                reason: Some(reason.clone()),
205                kind: ExecutionFailureKind::OutOfFuel,
206                detail: None,
207            },
208            FatalError::ParamsParsingError(err) => FinishedExecutionFailure {
209                reason: Some(reason_generic),
210                kind: ExecutionFailureKind::Uncategorized,
211                detail: err.detail(),
212            },
213            FatalError::CannotInstantiate { reason, detail } => FinishedExecutionFailure {
214                reason: Some(reason.clone()),
215                kind: ExecutionFailureKind::Uncategorized,
216                detail: detail.clone(),
217            },
218            FatalError::ResultParsingError(_) | FatalError::ConstraintViolation { reason: _ } => {
219                FinishedExecutionFailure {
220                    reason: Some(reason_generic),
221                    kind: ExecutionFailureKind::Uncategorized,
222                    detail: None,
223                }
224            }
225            FatalError::ImportedFunctionCallError { detail, .. }
226            | FatalError::WorkflowTrap { detail, .. } => FinishedExecutionFailure {
227                reason: Some(reason_generic),
228                kind: ExecutionFailureKind::Uncategorized,
229                detail: detail.clone(),
230            },
231            FatalError::Cancelled => FinishedExecutionFailure {
232                kind: ExecutionFailureKind::Cancelled,
233                reason: None,
234                detail: None,
235            },
236        }
237    }
238}