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