From eca9e2fabeb75acd872399a951e3221df92ee1ff Mon Sep 17 00:00:00 2001 From: zerosnacks <95942363+zerosnacks@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:24:28 +0200 Subject: [PATCH] Preserve task executor error types with associated errors --- examples/provisioned_executor.rs | 2 ++ src/error.rs | 12 ++++++++++++ src/lib.rs | 2 +- src/worker.rs | 14 ++++++++++++-- tests/task_executors.rs | 4 ++++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/examples/provisioned_executor.rs b/examples/provisioned_executor.rs index dd846fe..9786485 100644 --- a/examples/provisioned_executor.rs +++ b/examples/provisioned_executor.rs @@ -70,6 +70,8 @@ impl SandboxExecutor { } impl TaskExecutor for SandboxExecutor { + type Error = Error; + fn execute( &self, input: AgentTurnInput, diff --git a/src/error.rs b/src/error.rs index 6095e75..d2950c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,9 @@ use thiserror::Error; use crate::types::TaskId; +/// Boxed error returned by task execution code. +pub type BoxError = Box; + /// Result type used by Steda. pub type Result = std::result::Result; @@ -86,12 +89,21 @@ pub enum Error { #[error("serialization error: {0}")] Serialization(#[from] serde_json::Error), + /// Error returned by task execution code outside Steda's own error domain. + #[error("{0}")] + Task(#[source] BoxError), + /// Catch-all error for cases that do not deserve a dedicated variant. #[error("{0}")] Other(String), } impl Error { + /// Wrap an external task execution error while preserving its source chain. + pub fn task(error: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::Task(Box::new(error)) + } + /// Returns true when the error represents a deliberate task suspension. pub const fn is_suspended(&self) -> bool { matches!(self, Self::Suspended) diff --git a/src/lib.rs b/src/lib.rs index e9783dd..1f6d503 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -332,7 +332,7 @@ pub mod middleware { pub const SCHEMA_SQL: &str = include_str!("../sql/steda.sql"); pub use context::{TaskContext, TaskWait}; -pub use error::{Error, Result}; +pub use error::{BoxError, Error, Result}; pub use queue::Queue; pub use steda::{Steda, StedaBuilder}; pub use task::{Spawn, SpawnedTask, Task, TaskHandle, TaskRef, TaskSnapshot}; diff --git a/src/worker.rs b/src/worker.rs index 87d0aa3..b53a5ea 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -269,6 +269,8 @@ fn is_transient_sqlstate(code: &str) -> bool { /// struct ProvisionedExecutor; /// /// impl TaskExecutor for ProvisionedExecutor { +/// type Error = steda::Error; +/// /// fn execute( /// &self, /// input: i64, @@ -300,6 +302,12 @@ fn is_transient_sqlstate(code: &str) -> bool { /// must ensure dropping its future also terminates or fences that work so it cannot keep /// acting as the no-longer-owned attempt. pub trait TaskExecutor: Send + Sync + 'static { + /// Error returned by this executor. + /// + /// Steda converts this once at the worker boundary, allowing executor + /// implementations to use their own domain error type internally. + type Error: Into + Send + 'static; + /// Execute one typed task attempt. /// /// # Errors @@ -310,7 +318,7 @@ pub trait TaskExecutor: Send + Sync + 'static { &self, input: Input, context: TaskContext, - ) -> impl Future> + Send; + ) -> impl Future> + Send; } /// Reusable in-process async handler accepted by [`WorkerBuilder::task`]. @@ -337,6 +345,8 @@ impl TaskExecutor for F where F: TaskHandler, { + type Error = Error; + fn execute( &self, input: Input, @@ -488,7 +498,7 @@ impl WorkerBuilder { let executor = Arc::clone(&executor); Box::pin(async move { let input = serde_json::from_value::(raw)?; - let output = executor.execute(input, context).await?; + let output = executor.execute(input, context).await.map_err(Into::into)?; Ok(serde_json::to_value(output)?) }) }); diff --git a/tests/task_executors.rs b/tests/task_executors.rs index d0079de..1aa5ed9 100644 --- a/tests/task_executors.rs +++ b/tests/task_executors.rs @@ -48,6 +48,8 @@ mod tests { } impl TaskExecutor<(), ()> for CancellableExecutor { + type Error = Error; + fn execute( &self, (): (), @@ -72,6 +74,8 @@ mod tests { } impl TaskExecutor for ProvisionedExecutor { + type Error = Error; + fn execute( &self, input: i64,