orleans_rust_client/
error.rs1use crate::generated::pb;
4
5pub(crate) const BRIDGE_ERROR_TRAILER: &str = "bridge-error-bin";
8
9pub mod codes {
15 pub const UNKNOWN_GRAIN: &str = "unknown_grain";
17 pub const UNKNOWN_METHOD: &str = "unknown_method";
19 pub const INVALID_KEY: &str = "invalid_key";
21 pub const INVALID_PAYLOAD: &str = "invalid_payload";
23 pub const SERIALIZATION_ERROR: &str = "serialization_error";
25 pub const ORLEANS_REJECTION: &str = "orleans_rejection";
27 pub const ORLEANS_TIMEOUT: &str = "orleans_timeout";
29 pub const ORLEANS_UNAVAILABLE: &str = "orleans_unavailable";
31 pub const APPLICATION_ERROR: &str = "application_error";
33 pub const CANCELLED: &str = "cancelled";
35 pub const INTERNAL: &str = "internal";
37}
38
39#[derive(thiserror::Error, Debug)]
41#[non_exhaustive]
42pub enum OrleansError {
43 #[error("transport error: {0}")]
45 Transport(#[from] tonic::transport::Error),
46
47 #[error("grpc status: {0}")]
50 Status(#[from] tonic::Status),
51
52 #[error("serialization error: {0}")]
55 Serialization(String),
56
57 #[error("bridge error {code}: {message}")]
60 Bridge {
61 code: String,
63 message: String,
65 detail: Option<String>,
67 retryable: bool,
69 },
70
71 #[error("timeout")]
73 Timeout,
74
75 #[error("invalid configuration: {0}")]
77 InvalidConfig(String),
78}
79
80impl OrleansError {
81 pub(crate) fn from_status(status: tonic::Status) -> Self {
84 if let Some(bridge) = decode_bridge_error(&status) {
85 return OrleansError::Bridge {
86 code: bridge.code,
87 message: bridge.message,
88 detail: (!bridge.detail.is_empty()).then_some(bridge.detail),
89 retryable: bridge.retryable,
90 };
91 }
92
93 match status.code() {
94 tonic::Code::DeadlineExceeded => OrleansError::Timeout,
95 tonic::Code::Cancelled => OrleansError::Bridge {
96 code: codes::CANCELLED.to_owned(),
97 message: status.message().to_owned(),
98 detail: None,
99 retryable: false,
100 },
101 tonic::Code::Unavailable => OrleansError::Bridge {
102 code: codes::ORLEANS_UNAVAILABLE.to_owned(),
103 message: status.message().to_owned(),
104 detail: None,
105 retryable: true,
106 },
107 _ => OrleansError::Status(status),
108 }
109 }
110
111 #[must_use]
114 pub fn is_retryable(&self) -> bool {
115 match self {
116 OrleansError::Bridge { retryable, .. } => *retryable,
117 OrleansError::Status(status) => {
118 matches!(status.code(), tonic::Code::Unavailable)
119 }
120 OrleansError::Timeout => false,
121 _ => false,
122 }
123 }
124
125 #[must_use]
127 pub fn code(&self) -> Option<&str> {
128 match self {
129 OrleansError::Bridge { code, .. } => Some(code.as_str()),
130 _ => None,
131 }
132 }
133}
134
135fn decode_bridge_error(status: &tonic::Status) -> Option<pb::BridgeError> {
136 let value = status.metadata().get_bin(BRIDGE_ERROR_TRAILER)?;
137 let bytes = value.to_bytes().ok()?;
138 <pb::BridgeError as prost::Message>::decode(bytes).ok()
139}
140
141#[cfg(test)]
142mod tests {
143 use prost::Message as _;
144 use tonic::metadata::MetadataValue;
145
146 use super::*;
147
148 fn status_with_bridge_error(error: &pb::BridgeError) -> tonic::Status {
149 let mut status = tonic::Status::new(tonic::Code::Unimplemented, "boom");
150 let bytes = error.encode_to_vec();
151 status
152 .metadata_mut()
153 .insert_bin(BRIDGE_ERROR_TRAILER, MetadataValue::from_bytes(&bytes));
154 status
155 }
156
157 #[test]
158 fn decodes_structured_trailer() {
159 let bridge = pb::BridgeError {
160 code: codes::UNKNOWN_METHOD.to_owned(),
161 message: "no such method".to_owned(),
162 detail: String::new(),
163 retryable: false,
164 };
165 let error = OrleansError::from_status(status_with_bridge_error(&bridge));
166 assert_eq!(error.code(), Some(codes::UNKNOWN_METHOD));
167 assert!(!error.is_retryable());
168 match error {
169 OrleansError::Bridge {
170 message, detail, ..
171 } => {
172 assert_eq!(message, "no such method");
173 assert_eq!(detail, None);
174 }
175 other => panic!("expected bridge error, got {other:?}"),
176 }
177 }
178
179 #[test]
180 fn retryable_trailer_is_retryable() {
181 let bridge = pb::BridgeError {
182 code: codes::ORLEANS_REJECTION.to_owned(),
183 message: "rejected".to_owned(),
184 detail: "overloaded".to_owned(),
185 retryable: true,
186 };
187 let error = OrleansError::from_status(status_with_bridge_error(&bridge));
188 assert!(error.is_retryable());
189 assert!(matches!(
190 error,
191 OrleansError::Bridge {
192 detail: Some(_),
193 ..
194 }
195 ));
196 }
197
198 #[test]
199 fn maps_bare_status_codes() {
200 let timeout = OrleansError::from_status(tonic::Status::deadline_exceeded("late"));
201 assert!(matches!(timeout, OrleansError::Timeout));
202
203 let unavailable = OrleansError::from_status(tonic::Status::unavailable("down"));
204 assert_eq!(unavailable.code(), Some(codes::ORLEANS_UNAVAILABLE));
205 assert!(unavailable.is_retryable());
206
207 let other = OrleansError::from_status(tonic::Status::internal("oops"));
208 assert!(matches!(other, OrleansError::Status(_)));
209 assert_eq!(other.code(), None);
210 }
211
212 #[test]
213 fn cancelled_status_maps_to_cancelled_code() {
214 let err = OrleansError::from_status(tonic::Status::cancelled("stop"));
215 assert_eq!(err.code(), Some(codes::CANCELLED));
216 assert!(!err.is_retryable());
217 }
218
219 #[test]
220 fn display_renders_each_variant() {
221 let bridge = OrleansError::Bridge {
222 code: "orleans_timeout".to_owned(),
223 message: "boom".to_owned(),
224 detail: None,
225 retryable: false,
226 };
227 assert_eq!(bridge.to_string(), "bridge error orleans_timeout: boom");
228
229 let status = OrleansError::Status(tonic::Status::internal("nope"));
230 assert!(status.to_string().starts_with("grpc status"));
231
232 assert!(
233 OrleansError::Serialization("bad".to_owned())
234 .to_string()
235 .contains("serialization error")
236 );
237 assert!(
238 OrleansError::InvalidConfig("x".to_owned())
239 .to_string()
240 .contains("invalid configuration")
241 );
242 }
243
244 #[test]
245 fn non_bridge_errors_have_no_code_and_are_not_retryable() {
246 let serialization = OrleansError::Serialization("bad".to_owned());
247 assert_eq!(serialization.code(), None);
248 assert!(!serialization.is_retryable());
249 assert!(serialization.to_string().contains("serialization error"));
250
251 let config = OrleansError::InvalidConfig("nope".to_owned());
252 assert_eq!(config.code(), None);
253 assert!(!config.is_retryable());
254
255 assert_eq!(OrleansError::Timeout.to_string(), "timeout");
256 assert!(!OrleansError::Timeout.is_retryable());
257 }
258}