Skip to content

Commit 21263fa

Browse files
💥 feat(sdk): failure converter + rich error types (#1226)
* initial plan * Add ApplicationFailure and reshape ActivityError * Move shared errors and split data_converters * Implement erased failure classification * Route workflow/activity failures through converter and update nexus async assertion * fix: do not nest application failures * doc: failure converter plan * add activity error hint * add child workflow decoding * child workflow signal decode * use outgoing error type * differentiate workflow failures * enumerate all current failure info options * hook up query/update handlers * richer failure error shapes * activity errors tightening * surface cancellation failure info * enhance child workflow execution error * child signal error type * update docs * flesh out activity failure * prep for child wfl split * split child start/execution error type * refactor tests * failure converter refactor * add module doc comment * remove arch docs * collapse activity failure branches * pr feedback * switch from mod.rs to named module file * serialize details to payload * chore: add ergonomic constructors for cancellation details * remove arch doc * fix cause drops on conversion * remove redundant test * use payload converter from data converter * simplify workflow_message_to_failure * remove useless LA unit test * include basic callout of failure conversion in readme * fix lints * fix formatting * add identitiy to cancellation * fix merge conflict * special case ApplicationFailure during ActivityError construciton
1 parent 6f7e753 commit 21263fa

23 files changed

Lines changed: 4384 additions & 417 deletions

crates/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,6 @@ workspace = true
107107

108108
[dev-dependencies]
109109
futures-util = { version = "0.3", default-features = false }
110+
rstest = "0.26"
110111
tempfile = "3.21"
111112
tokio = { version = "1.47", features = ["macros", "rt"] }

crates/common/src/data_converters.rs

Lines changed: 133 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
//! Contains traits for and default implementations of data converters, codecs, and other
22
//! serialization related functionality.
33
4-
use crate::protos::temporal::api::{common::v1::Payload, failure::v1::Failure};
4+
mod failure_converter;
5+
6+
pub use failure_converter::{
7+
ActivityExecutionDecodeHint, ChildWorkflowExecutionDecodeHint, ChildWorkflowSignalDecodeHint,
8+
ChildWorkflowStartDecodeHint, DefaultFailureConverter, FailureConverter, FailureDecodeHint,
9+
};
10+
11+
use crate::protos::temporal::api::common::v1::Payload;
512
use futures::{FutureExt, future::BoxFuture};
613
use std::{collections::HashMap, sync::Arc};
714

@@ -22,6 +29,7 @@ impl std::fmt::Debug for DataConverter {
2229
.finish_non_exhaustive()
2330
}
2431
}
32+
2533
impl DataConverter {
2634
/// Create a new DataConverter with the given payload converter, failure converter, and codec.
2735
pub fn new(
@@ -105,6 +113,34 @@ impl DataConverter {
105113
&self.payload_converter
106114
}
107115

116+
/// Returns the failure converter component of this data converter.
117+
pub fn failure_converter(&self) -> &(dyn FailureConverter + Send + Sync) {
118+
self.failure_converter.as_ref()
119+
}
120+
121+
/// Decode a Temporal failure into a caller-facing Rust error surface.
122+
pub fn to_error<H: FailureDecodeHint>(
123+
&self,
124+
context: &SerializationContextData,
125+
failure: crate::protos::temporal::api::failure::v1::Failure,
126+
hint: H,
127+
) -> Result<H::Output, PayloadConversionError> {
128+
let normalized =
129+
self.failure_converter
130+
.to_error(failure, &self.payload_converter, context)?;
131+
Ok(hint.adapt(normalized))
132+
}
133+
134+
/// Encode a typed Rust error surface into a Temporal failure.
135+
pub fn to_failure(
136+
&self,
137+
context: &SerializationContextData,
138+
error: crate::error::OutgoingError,
139+
) -> crate::protos::temporal::api::failure::v1::Failure {
140+
self.failure_converter
141+
.to_failure(error, &self.payload_converter, context)
142+
}
143+
108144
/// Returns the codec component of this data converter.
109145
pub fn codec(&self) -> &(dyn PayloadCodec + Send + Sync) {
110146
self.codec.as_ref()
@@ -196,26 +232,6 @@ impl std::error::Error for PayloadConversionError {
196232
}
197233
}
198234

199-
/// Converts between Rust errors and Temporal [`Failure`] protobufs.
200-
pub trait FailureConverter {
201-
/// Convert an error into a Temporal failure protobuf.
202-
fn to_failure(
203-
&self,
204-
error: Box<dyn std::error::Error>,
205-
payload_converter: &PayloadConverter,
206-
context: &SerializationContextData,
207-
) -> Result<Failure, PayloadConversionError>;
208-
209-
/// Convert a Temporal failure protobuf back into a Rust error.
210-
fn to_error(
211-
&self,
212-
failure: Failure,
213-
payload_converter: &PayloadConverter,
214-
context: &SerializationContextData,
215-
) -> Result<Box<dyn std::error::Error>, PayloadConversionError>;
216-
}
217-
/// Default (currently unimplemented) failure converter.
218-
pub struct DefaultFailureConverter;
219235
/// Encodes and decodes payloads, enabling encryption or compression.
220236
pub trait PayloadCodec {
221237
/// Encode payloads before they are sent to the server.
@@ -307,6 +323,53 @@ pub trait TemporalDeserializable: Sized {
307323
}
308324
}
309325

326+
/// A codec-decoded set of payloads that can be deserialized later with to a user provided type.
327+
#[derive(Clone, Debug)]
328+
pub struct DecodablePayloads {
329+
payloads: Vec<Payload>,
330+
payload_converter: PayloadConverter,
331+
context: SerializationContextData,
332+
}
333+
334+
impl DecodablePayloads {
335+
/// Create a new decodable payload set from raw payloads and the converter context needed to
336+
/// deserialize them later.
337+
pub fn new(
338+
payloads: Vec<Payload>,
339+
payload_converter: PayloadConverter,
340+
context: SerializationContextData,
341+
) -> Self {
342+
Self {
343+
payloads,
344+
payload_converter,
345+
context,
346+
}
347+
}
348+
349+
/// Deserialize these payloads into a typed value using the stored payload converter.
350+
pub fn deserialize<T: TemporalDeserializable + 'static>(
351+
&self,
352+
) -> Result<T, PayloadConversionError> {
353+
self.payload_converter.from_payloads(
354+
&SerializationContext {
355+
data: &self.context,
356+
converter: &self.payload_converter,
357+
},
358+
self.payloads.clone(),
359+
)
360+
}
361+
362+
/// Returns the underlying payloads.
363+
pub fn raw(&self) -> &[Payload] {
364+
&self.payloads
365+
}
366+
367+
/// Consume this value and return the underlying payloads as a [`RawValue`].
368+
pub fn into_raw(self) -> RawValue {
369+
RawValue::new(self.payloads)
370+
}
371+
}
372+
310373
/// An unconverted set of payloads, used when the caller wants to defer deserialization.
311374
#[derive(Clone, Debug, Default)]
312375
pub struct RawValue {
@@ -672,24 +735,6 @@ impl Default for DataConverter {
672735
)
673736
}
674737
}
675-
impl FailureConverter for DefaultFailureConverter {
676-
fn to_failure(
677-
&self,
678-
_: Box<dyn std::error::Error>,
679-
_: &PayloadConverter,
680-
_: &SerializationContextData,
681-
) -> Result<Failure, PayloadConversionError> {
682-
todo!()
683-
}
684-
fn to_error(
685-
&self,
686-
_: Failure,
687-
_: &PayloadConverter,
688-
_: &SerializationContextData,
689-
) -> Result<Box<dyn std::error::Error>, PayloadConversionError> {
690-
todo!()
691-
}
692-
}
693738
impl PayloadCodec for DefaultPayloadCodec {
694739
fn encode(
695740
&self,
@@ -866,4 +911,53 @@ mod tests {
866911
let args: MultiArgs2<String, i32> = ("hello".to_string(), 42i32).into();
867912
assert_eq!(args, MultiArgs2("hello".to_string(), 42));
868913
}
914+
915+
fn decodable_from_value<T: TemporalSerializable + 'static>(value: &T) -> DecodablePayloads {
916+
let converter = PayloadConverter::default();
917+
let payloads = converter
918+
.to_payloads(
919+
&SerializationContext {
920+
data: &SerializationContextData::Workflow,
921+
converter: &converter,
922+
},
923+
value,
924+
)
925+
.unwrap();
926+
DecodablePayloads::new(payloads, converter, SerializationContextData::Workflow)
927+
}
928+
#[test]
929+
fn decodable_payloads_roundtrip_string() {
930+
let payloads = decodable_from_value(&"hello".to_string());
931+
932+
let result: String = payloads.deserialize().unwrap();
933+
934+
assert_eq!(result, "hello");
935+
}
936+
937+
#[test]
938+
fn decodable_payloads_roundtrip_option_string() {
939+
let payloads = decodable_from_value(&Some("hello".to_string()));
940+
941+
let result: Option<String> = payloads.deserialize().unwrap();
942+
943+
assert_eq!(result, Some("hello".to_string()));
944+
}
945+
946+
#[test]
947+
fn decodable_payloads_roundtrip_unit() {
948+
let payloads = decodable_from_value(&());
949+
950+
let result: () = payloads.deserialize().unwrap();
951+
952+
assert_eq!(result, ());
953+
}
954+
955+
#[test]
956+
fn decodable_payloads_roundtrip_vec_string() {
957+
let payloads = decodable_from_value(&vec!["hello".to_string(), "world".to_string()]);
958+
959+
let result: Vec<String> = payloads.deserialize().unwrap();
960+
961+
assert_eq!(result, vec!["hello".to_string(), "world".to_string()]);
962+
}
869963
}

0 commit comments

Comments
 (0)