Skip to content

Commit 0b60cd6

Browse files
fix cause drops on conversion
1 parent 65ec04a commit 0b60cd6

3 files changed

Lines changed: 162 additions & 59 deletions

File tree

crates/common/src/data_converters/failure_converter.rs

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,8 @@ impl FailureDecodeHint for ChildWorkflowSignalDecodeHint {
154154

155155
fn adapt(self, normalized: IncomingError) -> Self::Output {
156156
let failure = normalized.failure().clone();
157-
let payload_converter = PayloadConverter::default();
158-
let cause = failure.cause.clone().map(|cause| {
159-
decode_failure(*cause, &payload_converter, &SerializationContextData::None)
160-
});
161157
ChildWorkflowSignalError::Failed(Box::new(ChildWorkflowSignalFailureError::new(
162-
failure, normalized, cause,
158+
failure, normalized,
163159
)))
164160
}
165161
}
@@ -314,7 +310,10 @@ impl EncodeFailure for ApplicationFailure {
314310
.transpose()?;
315311
Ok(Failure {
316312
message: self.to_string(),
317-
cause: encode_application_failure_cause(self.source_error().as_ref()),
313+
cause: self
314+
.cause()
315+
.map(|cause| Box::new(cause.failure().clone()))
316+
.or_else(|| encode_application_failure_cause(self.source_error().as_ref())),
318317
failure_info: Some(FailureInfo::ApplicationFailureInfo(
319318
ApplicationFailureInfo {
320319
r#type: self.type_name().unwrap_or_default().to_owned(),
@@ -785,6 +784,30 @@ mod tests {
785784
assert_eq!(converted.cause.unwrap().as_ref(), &activity_failure);
786785
}
787786

787+
#[test]
788+
fn application_failures_fall_back_to_source_error() {
789+
let activity_failure = Failure {
790+
message: "activity failed".to_owned(),
791+
failure_info: Some(FailureInfo::ActivityFailureInfo(
792+
ActivityFailureInfo::default(),
793+
)),
794+
..Default::default()
795+
};
796+
let app = ApplicationFailure::new(anyhow::Error::new(ActivityExecutionError::Failed(
797+
ActivityFailureError::new(
798+
activity_failure.clone(),
799+
ActivityFailureInfo::default(),
800+
None,
801+
),
802+
)));
803+
804+
assert!(app.cause().is_none());
805+
806+
let converted = convert(OutgoingWorkflowError::Application(Box::new(app)));
807+
808+
assert_eq!(converted.cause.unwrap().as_ref(), &activity_failure);
809+
}
810+
788811
#[test]
789812
fn application_failures_skip_generic_wrappers_around_known_causes() {
790813
let activity_failure = Failure {
@@ -940,6 +963,48 @@ mod tests {
940963
assert!(matches!(app.cause(), Some(IncomingError::Timeout(_))));
941964
}
942965

966+
#[test]
967+
fn decoded_application_failures_preserve_cause() {
968+
let failure = Failure {
969+
message: "app boom".to_owned(),
970+
cause: Some(Box::new(timeout_failure("timed out"))),
971+
failure_info: Some(FailureInfo::ApplicationFailureInfo(
972+
ApplicationFailureInfo::default(),
973+
)),
974+
..Default::default()
975+
};
976+
977+
let decoded = DefaultFailureConverter
978+
.to_error(
979+
failure.clone(),
980+
&PayloadConverter::default(),
981+
&SerializationContextData::Workflow,
982+
)
983+
.unwrap();
984+
985+
let IncomingError::Application(app) = decoded else {
986+
panic!("expected application error");
987+
};
988+
assert!(app.as_timeout().is_some());
989+
990+
let reencoded = convert(OutgoingWorkflowError::Application(Box::new(app)));
991+
992+
assert_eq!(reencoded.message, failure.message);
993+
assert_eq!(reencoded.cause.as_deref(), failure.cause.as_deref());
994+
995+
let decoded_reencoded = DefaultFailureConverter
996+
.to_error(
997+
reencoded,
998+
&PayloadConverter::default(),
999+
&SerializationContextData::Workflow,
1000+
)
1001+
.unwrap();
1002+
let IncomingError::Application(roundtripped) = decoded_reencoded else {
1003+
panic!("expected application error");
1004+
};
1005+
assert!(roundtripped.as_timeout().is_some());
1006+
}
1007+
9431008
#[test]
9441009
fn application_failures_decode_wrapped_known_causes_without_collapsing_wrapper() {
9451010
let failure = Failure {

crates/common/src/error.rs

Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
33
use crate::{
44
data_converters::{
5-
DecodablePayloads, DefaultFailureConverter, FailureConverter, GenericPayloadConverter,
6-
PayloadConversionError, PayloadConverter, RawValue, SerializationContext,
7-
SerializationContextData, TemporalDeserializable, TemporalSerializable,
5+
DecodablePayloads, GenericPayloadConverter, PayloadConversionError, PayloadConverter,
6+
RawValue, SerializationContext, SerializationContextData, TemporalDeserializable,
7+
TemporalSerializable,
88
},
99
protos::{
1010
coresdk::child_workflow::StartChildWorkflowExecutionFailedCause,
@@ -210,6 +210,11 @@ impl ApplicationFailure {
210210
self.failure.as_ref()
211211
}
212212

213+
/// Consumes this application failure and returns the retained proto failure, if one exists.
214+
pub fn into_failure(self) -> Option<Failure> {
215+
self.failure
216+
}
217+
213218
/// Returns the normalized cause, if any.
214219
pub fn cause(&self) -> Option<&IncomingError> {
215220
self.cause.as_deref()
@@ -284,16 +289,6 @@ impl From<PayloadConversionError> for ApplicationFailure {
284289
}
285290
}
286291

287-
impl From<ApplicationFailure> for Failure {
288-
fn from(value: ApplicationFailure) -> Self {
289-
DefaultFailureConverter.to_failure(
290-
OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(value))),
291-
&PayloadConverter::default(),
292-
&SerializationContextData::None,
293-
)
294-
}
295-
}
296-
297292
/// A typed outbound error surface used before encoding to a Temporal failure proto.
298293
#[derive(Debug, thiserror::Error)]
299294
pub enum OutgoingError {
@@ -444,7 +439,9 @@ impl IncomingError {
444439
/// Consumes this normalized error and returns the retained proto failure.
445440
pub fn into_failure(self) -> Failure {
446441
match self {
447-
IncomingError::Application(err) => err.into(),
442+
IncomingError::Application(err) => err
443+
.into_failure()
444+
.expect("decoded application failures retain their original proto"),
448445
IncomingError::Timeout(err) => err.into_failure(),
449446
IncomingError::Cancelled(err) => err.into_failure(),
450447
IncomingError::Terminated(err) => err.into_failure(),
@@ -1038,38 +1035,53 @@ impl ChildWorkflowSignalError {
10381035
pub struct ChildWorkflowSignalFailureError {
10391036
failure: Failure,
10401037
error: Box<IncomingError>,
1041-
cause: Option<Box<IncomingError>>,
10421038
}
10431039

10441040
impl ChildWorkflowSignalFailureError {
10451041
/// Creates a child-workflow signal failure wrapper.
1046-
pub(crate) fn new(
1047-
failure: Failure,
1048-
error: IncomingError,
1049-
cause: Option<IncomingError>,
1050-
) -> Self {
1042+
pub(crate) fn new(failure: Failure, error: IncomingError) -> Self {
10511043
Self {
10521044
failure,
10531045
error: Box::new(error),
1054-
cause: cause.map(Box::new),
10551046
}
10561047
}
10571048

1049+
/// Returns the retained top-level proto failure.
1050+
pub fn failure(&self) -> &Failure {
1051+
&self.failure
1052+
}
1053+
1054+
/// Returns the normalized direct cause of the child-workflow signal failure, if any.
1055+
pub fn cause(&self) -> Option<&IncomingError> {
1056+
self.error.cause()
1057+
}
1058+
10581059
/// Returns the direct decoded incoming error represented by the top-level proto failure.
10591060
pub fn error(&self) -> &IncomingError {
10601061
&self.error
10611062
}
10621063
}
10631064

1064-
impl_incoming_failure_wrapper!(ChildWorkflowSignalFailureError);
1065+
impl std::fmt::Display for ChildWorkflowSignalFailureError {
1066+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1067+
self.failure.fmt(f)
1068+
}
1069+
}
1070+
1071+
impl std::error::Error for ChildWorkflowSignalFailureError {
1072+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1073+
self.cause()
1074+
.map(|cause| cause as &(dyn std::error::Error + 'static))
1075+
}
1076+
}
10651077

10661078
#[cfg(test)]
10671079
mod tests {
10681080
use super::*;
10691081
use crate::{
10701082
data_converters::{
1071-
GenericPayloadConverter, PayloadConverter, SerializationContext,
1072-
SerializationContextData,
1083+
DefaultFailureConverter, FailureConverter, GenericPayloadConverter, PayloadConverter,
1084+
SerializationContext, SerializationContextData,
10731085
},
10741086
protos::temporal::api::{common::v1::Payload, failure::v1::failure::FailureInfo},
10751087
};
@@ -1098,14 +1110,19 @@ mod tests {
10981110
..Default::default()
10991111
}],
11001112
};
1101-
let failure: Failure = ApplicationFailure::builder(anyhow::anyhow!("oops"))
1102-
.type_name("MyType".to_owned())
1103-
.non_retryable(true)
1104-
.next_retry_delay(Duration::from_secs(3))
1105-
.category(ApplicationErrorCategory::Benign)
1106-
.details(RawValue::new(payloads.payloads.clone()))
1107-
.build()
1108-
.into();
1113+
let failure = DefaultFailureConverter.to_failure(
1114+
OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1115+
ApplicationFailure::builder(anyhow::anyhow!("oops"))
1116+
.type_name("MyType".to_owned())
1117+
.non_retryable(true)
1118+
.next_retry_delay(Duration::from_secs(3))
1119+
.category(ApplicationErrorCategory::Benign)
1120+
.details(RawValue::new(payloads.payloads.clone()))
1121+
.build(),
1122+
))),
1123+
&PayloadConverter::default(),
1124+
&SerializationContextData::None,
1125+
);
11091126
let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
11101127
panic!("expected application failure info");
11111128
};
@@ -1123,10 +1140,15 @@ mod tests {
11231140
data: b"details".to_vec(),
11241141
..Default::default()
11251142
};
1126-
let failure: Failure = ApplicationFailure::builder(anyhow::anyhow!("oops"))
1127-
.details(RawValue::new(vec![payload.clone()]))
1128-
.build()
1129-
.into();
1143+
let failure = DefaultFailureConverter.to_failure(
1144+
OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1145+
ApplicationFailure::builder(anyhow::anyhow!("oops"))
1146+
.details(RawValue::new(vec![payload.clone()]))
1147+
.build(),
1148+
))),
1149+
&PayloadConverter::default(),
1150+
&SerializationContextData::None,
1151+
);
11301152

11311153
let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
11321154
panic!("expected application failure info");
@@ -1136,10 +1158,15 @@ mod tests {
11361158

11371159
#[test]
11381160
fn builder_accepts_serializable_details() {
1139-
let failure: Failure = ApplicationFailure::builder(anyhow::anyhow!("oops"))
1140-
.details("details".to_string())
1141-
.build()
1142-
.into();
1161+
let failure = DefaultFailureConverter.to_failure(
1162+
OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1163+
ApplicationFailure::builder(anyhow::anyhow!("oops"))
1164+
.details("details".to_string())
1165+
.build(),
1166+
))),
1167+
&PayloadConverter::default(),
1168+
&SerializationContextData::None,
1169+
);
11431170

11441171
let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
11451172
panic!("expected application failure info");
@@ -1159,11 +1186,16 @@ mod tests {
11591186
}
11601187

11611188
#[test]
1162-
fn application_failure_into_failure_surfaces_detail_encoding_errors() {
1163-
let failure: Failure = ApplicationFailure::builder(anyhow::anyhow!("oops"))
1164-
.details(AlwaysFailsSerialize)
1165-
.build()
1166-
.into();
1189+
fn application_failure_encoding_surfaces_detail_encoding_errors() {
1190+
let failure = DefaultFailureConverter.to_failure(
1191+
OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1192+
ApplicationFailure::builder(anyhow::anyhow!("oops"))
1193+
.details(AlwaysFailsSerialize)
1194+
.build(),
1195+
))),
1196+
&PayloadConverter::default(),
1197+
&SerializationContextData::None,
1198+
);
11671199

11681200
assert_eq!(
11691201
failure.message,

crates/common/src/payload_visitor.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ include!(concat!(env!("OUT_DIR"), "/payload_visitor_impl.rs"));
194194
mod tests {
195195
use super::*;
196196
use crate::{
197-
error::ApplicationFailure,
197+
data_converters::{DefaultFailureConverter, FailureConverter, PayloadConverter},
198+
error::{ApplicationFailure, OutgoingError, OutgoingWorkflowError},
198199
protos::{
199200
coresdk::{
200201
activity_result::{
@@ -215,7 +216,7 @@ mod tests {
215216
},
216217
temporal::api::{
217218
common::v1::{Memo, SearchAttributes},
218-
failure::v1::{Failure, failure::FailureInfo},
219+
failure::v1::failure::FailureInfo,
219220
},
220221
},
221222
};
@@ -654,12 +655,17 @@ mod tests {
654655

655656
#[tokio::test]
656657
async fn test_encode_failure_encodes_application_failure_details() {
657-
let mut failure: Failure = ApplicationFailure::builder(anyhow::anyhow!("app boom"))
658-
.details(crate::data_converters::RawValue::new(vec![make_payload(
659-
"detail",
660-
)]))
661-
.build()
662-
.into();
658+
let mut failure = DefaultFailureConverter.to_failure(
659+
OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
660+
ApplicationFailure::builder(anyhow::anyhow!("app boom"))
661+
.details(crate::data_converters::RawValue::new(vec![make_payload(
662+
"detail",
663+
)]))
664+
.build(),
665+
))),
666+
&PayloadConverter::default(),
667+
&SerializationContextData::Workflow,
668+
);
663669

664670
encode_payloads(
665671
&mut failure,

0 commit comments

Comments
 (0)