1use std::error::Error;
4
5use serde::{Deserialize, Serialize};
6use xmtp_common::{ErrorCode, RetryableError};
7
8#[cfg(not(target_arch = "wasm32"))]
9use super::catch_up::CatchUpError;
10use super::{
11 SubscribeError,
12 barrier::{BarrierCause, BarrierError, BarrierFailure, BarrierTopic},
13 incoming::IncomingError,
14};
15use crate::{
16 client::ClientError,
17 groups::{GroupError, mls_sync::GroupMessageProcessingError, summary::SyncSummary},
18};
19
20pub const STREAM_FAILURE_MARKER: &str = "\n[XMTP_STREAM_FAILURE_V1]";
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum StreamFailureKind {
27 Barrier,
28 PublishedButUnconfirmed,
29 CatchUp,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum StreamBarrierReason {
36 Blocked,
37 Deadline,
38 Cancelled,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum StreamBarrierCauseKind {
45 TargetPending,
46 ReceiptPending,
47 ProcessingPending,
48 Blocked,
49 Storage,
50 Receiver,
51 InvalidTopic,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct StreamBarrierCause {
58 pub kind: StreamBarrierCauseKind,
59 pub code: Option<String>,
61 pub message: String,
63 pub retryable: bool,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct StreamBarrierTopic {
70 pub topic: String,
72 pub target: Option<String>,
74 pub received: String,
76 pub processed: String,
78 pub unresolved_welcomes: Vec<String>,
80 pub inactive: bool,
81 pub cause: Option<StreamBarrierCause>,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct StreamBarrierFailure {
88 pub reason: StreamBarrierReason,
89 pub unfinished: Vec<StreamBarrierTopic>,
91}
92
93#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct StreamFailureSummary {
97 pub messages: String,
98 pub conversations: String,
99 pub failed: String,
100 pub completed: bool,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct StreamFailureDetails {
107 pub kind: StreamFailureKind,
108 pub code: String,
109 pub message: String,
110 pub retryable: bool,
111 pub intent_id: Option<i32>,
113 pub published_intent_ids: Vec<i32>,
115 pub summary: Option<StreamFailureSummary>,
117 pub barriers: Vec<StreamBarrierFailure>,
119}
120
121impl From<&BarrierCause> for StreamBarrierCause {
122 fn from(cause: &BarrierCause) -> Self {
123 use StreamBarrierCauseKind as Kind;
124 let (kind, code, message, retryable) = match cause {
125 BarrierCause::TargetPending => {
126 (Kind::TargetPending, None, "Target capture is pending", true)
127 }
128 BarrierCause::ReceiptPending => {
129 (Kind::ReceiptPending, None, "Receipt is pending", true)
130 }
131 BarrierCause::ProcessingPending => {
132 (Kind::ProcessingPending, None, "Processing is pending", true)
133 }
134 BarrierCause::Blocked(code) => (
135 Kind::Blocked,
136 Some(code.clone()),
137 "Processing is blocked",
138 false,
139 ),
140 BarrierCause::Storage(error) => (
141 Kind::Storage,
142 Some(error.error_code().to_string()),
143 "Storage failed",
144 error.is_retryable(),
145 ),
146 BarrierCause::Receiver(error) => (
147 Kind::Receiver,
148 Some(error.code().to_owned()),
149 "The receiver failed",
150 error.is_retryable(),
151 ),
152 BarrierCause::InvalidTopic => (Kind::InvalidTopic, None, "The topic is invalid", false),
153 };
154 Self {
155 kind,
156 code,
157 message: message.to_owned(),
158 retryable,
159 }
160 }
161}
162
163impl From<&BarrierTopic> for StreamBarrierTopic {
164 fn from(topic: &BarrierTopic) -> Self {
165 Self {
166 topic: hex::encode(topic.topic.cloned_vec()),
167 target: topic.target.map(|cursor| cursor.0.to_string()),
168 received: topic.received.0.to_string(),
169 processed: topic.processed.0.to_string(),
170 unresolved_welcomes: topic
171 .unresolved_welcomes
172 .iter()
173 .map(|cursor| cursor.0.to_string())
174 .collect(),
175 inactive: topic.inactive,
176 cause: topic.cause.as_ref().map(Into::into),
177 }
178 }
179}
180
181impl From<&BarrierError> for StreamBarrierFailure {
182 fn from(error: &BarrierError) -> Self {
183 let BarrierError::Incomplete { reason, unfinished } = error;
184 Self {
185 reason: match reason {
186 BarrierFailure::Blocked => StreamBarrierReason::Blocked,
187 BarrierFailure::Deadline => StreamBarrierReason::Deadline,
188 BarrierFailure::Cancelled => StreamBarrierReason::Cancelled,
189 },
190 unfinished: unfinished.iter().map(Into::into).collect(),
191 }
192 }
193}
194
195impl StreamFailureDetails {
196 fn barrier(error: &BarrierError) -> Self {
197 Self {
198 kind: StreamFailureKind::Barrier,
199 code: error.error_code().to_string(),
200 message: "Processing barriers did not complete".into(),
201 retryable: error.is_retryable(),
202 intent_id: None,
203 published_intent_ids: Vec::new(),
204 summary: None,
205 barriers: vec![error.into()],
206 }
207 }
208}
209
210pub fn stream_failure_details(error: &(dyn Error + 'static)) -> Option<StreamFailureDetails> {
213 let mut pending = vec![(error, 0usize)];
214 let mut failures = Vec::new();
215 while let Some((error, depth)) = pending.pop() {
216 if depth >= 64 {
217 continue;
218 }
219 let mut push = |error| pending.push((error, depth + 1));
220 #[cfg(not(target_arch = "wasm32"))]
221 if let Some(error) = error.downcast_ref::<CatchUpError>() {
222 match error {
223 CatchUpError::Group(group) => push(group),
224 CatchUpError::Incomplete { summary, causes } => {
225 failures.push(StreamFailureDetails {
226 kind: StreamFailureKind::CatchUp,
227 code: error.error_code().to_string(),
228 message: "Catch-up did not complete".into(),
229 retryable: error.is_retryable(),
230 intent_id: None,
231 published_intent_ids: Vec::new(),
232 summary: Some(StreamFailureSummary {
233 messages: summary.messages.to_string(),
234 conversations: summary.conversations.to_string(),
235 failed: summary.failed.to_string(),
236 completed: summary.completed,
237 }),
238 barriers: causes.iter().map(Into::into).collect(),
239 })
240 }
241 }
242 continue;
243 }
244 if let Some(error) = error.downcast_ref::<GroupError>() {
245 match error {
246 GroupError::StreamBarrier(error) => {
247 failures.push(StreamFailureDetails::barrier(error))
248 }
249 GroupError::PublishedButUnconfirmed { intent_id, cause } => {
250 failures.push(StreamFailureDetails {
251 kind: StreamFailureKind::PublishedButUnconfirmed,
252 code: error.error_code().to_string(),
253 message: "The published intent is not confirmed".into(),
254 retryable: error.is_retryable(),
255 intent_id: Some(*intent_id),
256 published_intent_ids: vec![*intent_id],
257 summary: None,
258 barriers: cause.iter().map(|cause| cause.as_ref().into()).collect(),
259 })
260 }
261 GroupError::Sync(summary) | GroupError::SyncFailedToWait(summary) => {
262 push(summary.as_ref())
263 }
264 GroupError::Client(error) => push(error),
265 _ => {
266 if let Some(source) = error.source() {
267 push(source);
268 }
269 }
270 }
271 continue;
272 }
273 if let Some(error) = error.downcast_ref::<BarrierError>() {
274 failures.push(StreamFailureDetails::barrier(error));
275 continue;
276 }
277 if let Some(summary) = error.downcast_ref::<SyncSummary>() {
278 for (_, error) in summary.process.errored.iter().rev() {
279 push(error);
280 }
281 for error in summary.post_commit_errors.iter().rev() {
282 push(error);
283 }
284 for error in summary.publish_errors.iter().rev() {
285 push(error);
286 }
287 if let Some(error) = summary.other.as_deref() {
288 push(error);
289 }
290 continue;
291 }
292 if let Some(ClientError::Group(error)) = error.downcast_ref::<ClientError>() {
293 push(error.as_ref());
294 } else if let Some(SubscribeError::Group(error)) = error.downcast_ref::<SubscribeError>() {
295 push(error.as_ref());
296 } else if let Some(IncomingError::Group(error)) = error.downcast_ref::<IncomingError>() {
297 push(error);
298 } else if let Some(error) = error.downcast_ref::<GroupMessageProcessingError>() {
299 match error {
300 GroupMessageProcessingError::PreparedAttempt(error) => push(error.as_ref()),
301 GroupMessageProcessingError::Client(error) => push(error),
302 _ => {
303 if let Some(source) = error.source() {
304 push(source);
305 }
306 }
307 }
308 } else if let Some(source) = error.source() {
309 push(source);
310 }
311 }
312 let mut failures = failures.into_iter();
313 let mut result = failures.next()?;
314 for failure in failures {
315 result.retryable |= failure.retryable;
316 result.barriers.extend(failure.barriers);
317 result
318 .published_intent_ids
319 .extend(failure.published_intent_ids);
320 }
321 result.published_intent_ids.sort_unstable();
322 result.published_intent_ids.dedup();
323 result.intent_id = match result.published_intent_ids.as_slice() {
324 [id] => Some(*id),
325 _ => None,
326 };
327 Some(result)
328}
329
330pub fn encode_stream_failure(error: &(dyn Error + 'static)) -> Option<String> {
332 let details = stream_failure_details(error)?;
333 let json = serde_json::to_string(&details).ok()?;
334 Some(format!("{STREAM_FAILURE_MARKER}{json}"))
335}
336
337pub fn decode_stream_failure(message: &str) -> Option<StreamFailureDetails> {
339 let (_, json) = message.rsplit_once(STREAM_FAILURE_MARKER)?;
340 serde_json::from_str(json).ok()
341}
342
343#[cfg(test)]
344mod tests;