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