xmtp_proto/traits/combinators/
retry.rs1use std::marker::PhantomData;
2
3use xmtp_common::{
4 ExponentialBackoff, MaybeSend, MaybeSync, Retry, Strategy as RetryStrategy, retry_async,
5};
6
7use crate::api::{ApiClientError, Client, Endpoint, Pageable, Query, QueryRaw};
8
9pub struct RetryQuery<E, S = ExponentialBackoff> {
13 endpoint: E,
14 pub(crate) retry: Retry<S>,
15}
16
17impl<E> RetryQuery<E> {
18 pub fn new(endpoint: E) -> Self {
19 Self {
20 endpoint,
21 retry: Default::default(),
22 }
23 }
24}
25
26impl<E> Pageable for RetryQuery<E>
27where
28 E: Pageable,
29{
30 fn set_cursor(&mut self, cursor: u64) {
31 self.endpoint.set_cursor(cursor)
32 }
33}
34
35#[xmtp_common::async_trait]
36impl<E, C, S> Query<C> for RetryQuery<E, S>
37where
38 E: Query<C>,
39 C: Client,
40 S: RetryStrategy,
41{
42 type Output = E::Output;
43 async fn query(&mut self, client: &C) -> Result<Self::Output, ApiClientError> {
44 retry_async!(
45 self.retry,
46 (async { Query::<C>::query(&mut self.endpoint, client).await })
47 )
48 }
49}
50
51#[xmtp_common::async_trait]
52impl<E, C, S> QueryRaw<C> for RetryQuery<E, S>
53where
54 E: Endpoint,
55 C: Client,
56 S: RetryStrategy,
57{
58 async fn query_raw(&mut self, client: &C) -> Result<bytes::Bytes, ApiClientError> {
59 retry_async!(
60 self.retry,
61 (async { QueryRaw::<C>::query_raw(&mut self.endpoint, client).await })
62 )
63 }
64}
65
66pub struct RetrySpecialized<Spec> {
67 _marker: PhantomData<Spec>,
68}
69
70impl<E, Spec> Endpoint<RetrySpecialized<Spec>> for RetryQuery<E>
71where
72 E: Endpoint<Spec>,
73 Spec: MaybeSend + MaybeSync,
74{
75 type Output = <E as Endpoint<Spec>>::Output;
76
77 fn grpc_endpoint(&self) -> std::borrow::Cow<'static, str> {
78 self.endpoint.grpc_endpoint()
79 }
80
81 fn body(&self) -> Result<bytes::Bytes, crate::api::BodyError> {
82 self.endpoint.body()
83 }
84}
85
86pub fn retry<E>(endpoint: E) -> RetryQuery<E, ExponentialBackoff> {
88 RetryQuery::<E, _> {
89 endpoint,
90 retry: Retry::default(),
91 }
92}
93
94pub fn retry_with_strategy<E, S>(endpoint: E, retry: Retry<S>) -> RetryQuery<E, S> {
96 RetryQuery::<E, S> { endpoint, retry }
97}
98
99#[cfg(test)]
100mod tests {
101
102 use crate::api::{
103 EndpointExt,
104 mock::{MockError, MockNetworkClient, TestEndpoint},
105 };
106
107 use super::*;
108
109 #[xmtp_common::test]
110 async fn retries_endpoint_three_times() {
111 let mut client = MockNetworkClient::new();
112 client.expect_request().times(3).returning(|_, _, _| {
113 tracing::info!("error");
114 Err(ApiClientError::client(MockError::ARetryableError))
115 });
116 client
117 .expect_request()
118 .times(1)
119 .returning(|_, _, _| Ok(http::Response::new(vec![].into())));
120
121 let result: Result<(), _> = retry(TestEndpoint).query(&client).await;
122 assert!(result.is_ok());
123 }
124
125 #[xmtp_common::test]
126 async fn does_not_retry_non_retryable() {
127 let mut client = MockNetworkClient::new();
128 client
129 .expect_request()
130 .times(1)
131 .returning(|_, _, _| Err(ApiClientError::client(MockError::ANonRetryableError)));
132
133 let result: Result<(), _> = retry(TestEndpoint).query(&client).await;
134 assert!(result.is_err());
135 }
136
137 #[xmtp_common::test]
138 fn test_grpc_endpoint_delegates_to_wrapped_endpoint() {
139 let retry_endpoint = retry(TestEndpoint);
140 assert_eq!(retry_endpoint.grpc_endpoint(), "");
141 }
142
143 #[xmtp_common::test]
144 fn test_body_delegates_to_wrapped_endpoint() {
145 let retry_endpoint = retry(TestEndpoint);
146 let result = retry_endpoint.body();
147 assert!(result.is_ok());
148 assert_eq!(result.unwrap(), bytes::Bytes::from(vec![]));
149 }
150
151 #[xmtp_common::test]
152 async fn retries_with_strategy() {
153 let mut client = MockNetworkClient::new();
154 client
155 .expect_request()
156 .times(2)
157 .returning(|_, _, _| Err(ApiClientError::client(MockError::ARetryableError)));
158 client
159 .expect_request()
160 .times(1)
161 .returning(|_, _, _| Ok(http::Response::new(vec![1].into())));
162
163 let result: Result<(), _> = TestEndpoint
164 .ignore_response() .retry_with_strategy(Retry::builder().retries(2).build())
166 .query(&client)
167 .await;
168 assert!(result.is_ok(), "{:?}", result.unwrap_err());
169 }
170}