1use std::{
5 collections::BTreeSet,
6 fmt,
7 future::Future,
8 iter,
9 sync::{
10 atomic::{AtomicU32, Ordering},
11 Arc,
12 },
13};
14
15use futures::{future, stream, StreamExt};
16use linera_base::{
17 crypto::CryptoHash,
18 data_types::{BlobContent, BlockHeight, NetworkDescription},
19 ensure,
20 identifiers::{BlobId, ChainId, EventId},
21 time::{Duration, Instant},
22};
23use linera_chain::{
24 data_types::{self},
25 types::{
26 self, Certificate, ConfirmedBlock, ConfirmedBlockCertificate, GenericCertificate,
27 LiteCertificate, Timeout, ValidatedBlockCertificate,
28 },
29};
30#[cfg(with_metrics)]
31pub(crate) mod metrics {
32 use linera_base::prometheus_util::register_int_counter_vec;
33 use prometheus::IntCounterVec;
34
35 linera_base::declare_metrics! {
36 pub static VALIDATOR_SUBSCRIPTION_ERRORS: IntCounterVec =
37 register_int_counter_vec(
38 "validator_subscription_errors",
39 "Number of notification subscription stream errors per validator",
40 &["address"],
41 );
42 }
43}
44
45use linera_core::{
46 data_types::{CertificatesByHeightRequest, ChainInfoResponse},
47 node::{BlobStream, CrossChainMessageDelivery, NodeError, NotificationStream, ValidatorNode},
48 worker::Notification,
49};
50use linera_storage::Arc as CacheArc;
51use linera_version::VersionInfo;
52use tonic::{Code, IntoRequest, Request, Status};
53use tracing::{debug, instrument, trace, Level};
54
55use super::{
56 api::{self, validator_node_client::ValidatorNodeClient, SubscriptionRequest},
57 transport, GRPC_MAX_MESSAGE_SIZE,
58};
59#[cfg(feature = "opentelemetry")]
60use crate::propagation::{get_context_with_traffic_type, inject_context};
61use crate::{
62 grpc::api::RawCertificate, HandleConfirmedCertificateRequest, HandleLiteCertRequest,
63 HandleTimeoutCertificateRequest, HandleValidatedCertificateRequest,
64};
65
66const RECEIVED_STATUS_PREFIX: &str = "while receiving response with status: ";
68
69fn proxy_http_status(message: &str) -> Option<u16> {
72 message
73 .rsplit_once(RECEIVED_STATUS_PREFIX)
74 .and_then(|(_, rest)| rest.split_whitespace().next())
75 .and_then(|code| code.parse().ok())
76}
77
78fn is_server_error(status: u16) -> bool {
79 (500..600).contains(&status)
80}
81
82#[derive(Clone)]
84pub struct GrpcClient {
85 address: String,
86 client: ValidatorNodeClient<transport::Channel>,
87 retry_delay: Duration,
88 max_retries: u32,
89 max_backoff: Duration,
90 subscription_cooldowns: Arc<papaya::HashMap<String, Instant>>,
94}
95
96impl GrpcClient {
97 pub fn new(
99 address: String,
100 channel: transport::Channel,
101 retry_delay: Duration,
102 max_retries: u32,
103 max_backoff: Duration,
104 subscription_cooldowns: Arc<papaya::HashMap<String, Instant>>,
105 ) -> Self {
106 let client = ValidatorNodeClient::new(channel)
107 .max_encoding_message_size(GRPC_MAX_MESSAGE_SIZE)
108 .max_decoding_message_size(GRPC_MAX_MESSAGE_SIZE);
109 Self {
110 address,
111 client,
112 retry_delay,
113 max_retries,
114 max_backoff,
115 subscription_cooldowns,
116 }
117 }
118
119 pub fn address(&self) -> &str {
121 &self.address
122 }
123
124 fn is_retryable(status: &Status) -> bool {
127 match status.code() {
128 Code::DeadlineExceeded | Code::Aborted | Code::Unavailable | Code::Unknown => {
129 trace!("gRPC request interrupted: {status:?}; retrying");
130 true
131 }
132 Code::Ok | Code::Cancelled | Code::ResourceExhausted => {
133 trace!("Unexpected gRPC status: {status:?}; retrying");
134 true
135 }
136 Code::Internal if status.message().contains("h2 protocol error") => {
137 trace!("gRPC connection reset: {status:?}; retrying");
141 true
142 }
143 Code::Internal if proxy_http_status(status.message()).is_some_and(is_server_error) => {
144 trace!("gRPC proxy error: {status:?}; retrying");
149 true
150 }
151 Code::NotFound => false, Code::InvalidArgument
153 | Code::AlreadyExists
154 | Code::PermissionDenied
155 | Code::FailedPrecondition
156 | Code::OutOfRange
157 | Code::Unimplemented
158 | Code::Internal
159 | Code::DataLoss
160 | Code::Unauthenticated => {
161 trace!("Unexpected gRPC status: {status:?}");
162 false
163 }
164 }
165 }
166
167 async fn delegate<F, Fut, R, S>(
168 &self,
169 f: F,
170 request: impl TryInto<R> + fmt::Debug + Clone,
171 handler: &str,
172 ) -> Result<S, NodeError>
173 where
174 F: Fn(ValidatorNodeClient<transport::Channel>, Request<R>) -> Fut,
175 Fut: Future<Output = Result<tonic::Response<S>, Status>>,
176 R: IntoRequest<R> + Clone,
177 {
178 let mut retry_count = 0;
179 let request_inner = request.try_into().map_err(|_| NodeError::GrpcError {
180 error: "could not convert request to proto".to_string(),
181 })?;
182 loop {
183 #[allow(unused_mut)]
184 let mut request = Request::new(request_inner.clone());
185 #[cfg(feature = "opentelemetry")]
189 inject_context(&get_context_with_traffic_type(), request.metadata_mut());
190 match f(self.client.clone(), request).await {
191 Err(s) if Self::is_retryable(&s) && retry_count < self.max_retries => {
192 let delay = crate::jittered_backoff_delay(
193 self.retry_delay,
194 retry_count,
195 self.max_backoff,
196 );
197 retry_count += 1;
198 linera_base::time::timer::sleep(delay).await;
199 continue;
200 }
201 Err(s) => {
202 return Err(NodeError::GrpcError {
203 error: format!("remote request [{handler}] failed with status: {s:?}"),
204 });
205 }
206 Ok(result) => return Ok(result.into_inner()),
207 };
208 }
209 }
210
211 fn try_into_chain_info(
212 result: api::ChainInfoResult,
213 ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
214 let inner = result.inner.ok_or_else(|| NodeError::GrpcError {
215 error: "missing body from response".to_string(),
216 })?;
217 match inner {
218 api::chain_info_result::Inner::ChainInfoResponse(response) => {
219 Ok(response.try_into().map_err(|err| NodeError::GrpcError {
220 error: format!("failed to unmarshal response: {err}"),
221 })?)
222 }
223 api::chain_info_result::Inner::Error(error) => Err(bincode::deserialize(&error)
224 .map_err(|err| NodeError::GrpcError {
225 error: format!("failed to unmarshal error message: {err}"),
226 })?),
227 }
228 }
229}
230
231impl TryFrom<api::PendingBlobResult> for BlobContent {
232 type Error = NodeError;
233
234 fn try_from(result: api::PendingBlobResult) -> Result<Self, Self::Error> {
235 let inner = result.inner.ok_or_else(|| NodeError::GrpcError {
236 error: "missing body from response".to_string(),
237 })?;
238 match inner {
239 api::pending_blob_result::Inner::Blob(blob) => {
240 Ok(blob.try_into().map_err(|err| NodeError::GrpcError {
241 error: format!("failed to unmarshal response: {err}"),
242 })?)
243 }
244 api::pending_blob_result::Inner::Error(error) => Err(bincode::deserialize(&error)
245 .map_err(|err| NodeError::GrpcError {
246 error: format!("failed to unmarshal error message: {err}"),
247 })?),
248 }
249 }
250}
251
252macro_rules! client_delegate {
253 ($self:ident, $handler:ident, $req:ident) => {{
254 debug!(
255 handler = stringify!($handler),
256 request = ?$req,
257 "sending gRPC request"
258 );
259 $self
260 .delegate(
261 |mut client, req| async move { client.$handler(req).await },
262 $req,
263 stringify!($handler),
264 )
265 .await
266 }};
267}
268
269impl ValidatorNode for GrpcClient {
270 type NotificationStream = NotificationStream;
271
272 fn address(&self) -> String {
273 self.address.clone()
274 }
275
276 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
277 async fn handle_block_proposal(
278 &self,
279 proposal: data_types::BlockProposal,
280 ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
281 GrpcClient::try_into_chain_info(client_delegate!(self, handle_block_proposal, proposal)?)
282 }
283
284 #[instrument(target = "grpc_client", skip_all, fields(address = self.address))]
285 async fn handle_lite_certificate(
286 &self,
287 certificate: types::LiteCertificate<'_>,
288 delivery: CrossChainMessageDelivery,
289 ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
290 let wait_for_outgoing_messages = delivery.wait_for_outgoing_messages();
291 let request = HandleLiteCertRequest {
292 certificate,
293 wait_for_outgoing_messages,
294 };
295 GrpcClient::try_into_chain_info(client_delegate!(self, handle_lite_certificate, request)?)
296 }
297
298 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
299 async fn handle_confirmed_certificate(
300 &self,
301 certificate: CacheArc<ConfirmedBlockCertificate>,
302 delivery: CrossChainMessageDelivery,
303 ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
304 let wait_for_outgoing_messages: bool = delivery.wait_for_outgoing_messages();
305 let request = HandleConfirmedCertificateRequest {
306 certificate: CacheArc::unwrap_or_clone(certificate),
307 wait_for_outgoing_messages,
308 };
309 GrpcClient::try_into_chain_info(client_delegate!(
310 self,
311 handle_confirmed_certificate,
312 request
313 )?)
314 }
315
316 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
317 async fn handle_validated_certificate(
318 &self,
319 certificate: ValidatedBlockCertificate,
320 ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
321 let request = HandleValidatedCertificateRequest { certificate };
322 GrpcClient::try_into_chain_info(client_delegate!(
323 self,
324 handle_validated_certificate,
325 request
326 )?)
327 }
328
329 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
330 async fn handle_timeout_certificate(
331 &self,
332 certificate: GenericCertificate<Timeout>,
333 ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
334 let request = HandleTimeoutCertificateRequest { certificate };
335 GrpcClient::try_into_chain_info(client_delegate!(
336 self,
337 handle_timeout_certificate,
338 request
339 )?)
340 }
341
342 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
343 async fn handle_chain_info_query(
344 &self,
345 query: linera_core::data_types::ChainInfoQuery,
346 ) -> Result<linera_core::data_types::ChainInfoResponse, NodeError> {
347 GrpcClient::try_into_chain_info(client_delegate!(self, handle_chain_info_query, query)?)
348 }
349
350 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
351 async fn subscribe(&self, chains: Vec<ChainId>) -> Result<Self::NotificationStream, NodeError> {
352 let retry_delay = self.retry_delay;
353 let max_retries = self.max_retries;
354 let max_backoff = self.max_backoff;
355 let address = self.address.clone();
356 let subscription_cooldowns = self.subscription_cooldowns.clone();
357
358 {
361 let pinned = subscription_cooldowns.pin();
362 if let Some(&last_failure) = pinned.get(&address) {
363 if last_failure.elapsed() < max_backoff {
364 return Err(NodeError::SubscriptionFailed {
365 status: format!(
366 "validator {address} on cooldown after recent subscription failure"
367 ),
368 });
369 }
370 }
371 }
372
373 let retry_count = Arc::new(AtomicU32::new(0));
375 let subscription_request = SubscriptionRequest {
376 chain_ids: chains.into_iter().map(|chain| chain.into()).collect(),
377 };
378 let mut client = self.client.clone();
379
380 let mut stream = Some(
382 client
383 .subscribe(subscription_request.clone())
384 .await
385 .map_err(|status| {
386 subscription_cooldowns
387 .pin()
388 .insert(address.clone(), Instant::now());
389 NodeError::SubscriptionFailed {
390 status: status.to_string(),
391 }
392 })?
393 .into_inner(),
394 );
395
396 let retry_count_for_unfold = retry_count.clone();
399 let cooldowns_for_unfold = subscription_cooldowns.clone();
400 let address_for_unfold = address.clone();
401 let endlessly_retrying_notification_stream = stream::unfold((), move |()| {
402 let mut client = client.clone();
403 let subscription_request = subscription_request.clone();
404 let mut stream = stream.take();
405 let retry_count = retry_count_for_unfold.clone();
406 let cooldowns = cooldowns_for_unfold.clone();
407 let cooldown_address = address_for_unfold.clone();
408 async move {
409 let stream = if let Some(stream) = stream.take() {
410 future::Either::Right(stream)
411 } else {
412 match client.subscribe(subscription_request.clone()).await {
413 Err(err) => future::Either::Left(stream::iter(iter::once(Err(err)))),
414 Ok(response) => {
415 retry_count.store(0, Ordering::Relaxed);
417 cooldowns.pin().remove(&cooldown_address);
418 trace!("Successfully reconnected subscription stream");
419 future::Either::Right(response.into_inner())
420 }
421 }
422 };
423 Some((stream, ()))
424 }
425 })
426 .flatten();
427
428 let span = tracing::info_span!("notification stream");
429 #[cfg(with_metrics)]
430 let address_for_metrics = self.address.clone();
431 let cooldowns_for_take_while = subscription_cooldowns;
432 let address_for_take_while = self.address.clone();
433 let notification_stream = endlessly_retrying_notification_stream
436 .map(|result| {
437 Option::<Notification>::try_from(result?).map_err(|err| {
438 let message = format!("Could not deserialize notification: {err}");
439 tonic::Status::new(Code::Internal, message)
440 })
441 })
442 .take_while(move |result| {
443 let Err(status) = result else {
444 retry_count.store(0, Ordering::Relaxed);
445 return future::Either::Left(future::ready(true));
446 };
447
448 #[cfg(with_metrics)]
449 metrics::VALIDATOR_SUBSCRIPTION_ERRORS
450 .with_label_values(&[&address_for_metrics])
451 .inc();
452
453 let current_retry_count = retry_count.load(Ordering::Relaxed);
454 if !span.in_scope(|| Self::is_retryable(status))
455 || current_retry_count >= max_retries
456 {
457 cooldowns_for_take_while
458 .pin()
459 .insert(address_for_take_while.clone(), Instant::now());
460 return future::Either::Left(future::ready(false));
461 }
462 let delay =
463 crate::jittered_backoff_delay(retry_delay, current_retry_count, max_backoff);
464 retry_count.fetch_add(1, Ordering::Relaxed);
465 future::Either::Right(async move {
466 linera_base::time::timer::sleep(delay).await;
467 true
468 })
469 })
470 .filter_map(move |result| {
471 future::ready(match result {
472 Ok(notification @ Some(_)) => notification,
473 Ok(None) => None,
474 Err(err) => {
475 debug!(%address, "{}", err);
476 None
477 }
478 })
479 });
480
481 Ok(Box::pin(notification_stream))
482 }
483
484 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
485 async fn get_version_info(&self) -> Result<VersionInfo, NodeError> {
486 let req = ();
487 Ok(client_delegate!(self, get_version_info, req)?.into())
488 }
489
490 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
491 async fn get_network_description(&self) -> Result<NetworkDescription, NodeError> {
492 let req = ();
493 Ok(client_delegate!(self, get_network_description, req)?.try_into()?)
494 }
495
496 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
497 async fn upload_blob(&self, content: BlobContent) -> Result<BlobId, NodeError> {
498 Ok(client_delegate!(self, upload_blob, content)?.try_into()?)
499 }
500
501 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
502 async fn download_blob(&self, blob_id: BlobId) -> Result<BlobContent, NodeError> {
503 Ok(client_delegate!(self, download_blob, blob_id)?.try_into()?)
504 }
505
506 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
507 async fn download_blobs(&self, blob_ids: Vec<BlobId>) -> Result<BlobStream, NodeError> {
508 debug!(
509 handler = "download_blobs",
510 num_blobs = blob_ids.len(),
511 "sending gRPC request"
512 );
513 let request = api::BlobIds::try_from(blob_ids)?;
514 let stream = self
515 .client
516 .clone()
517 .download_blobs(request)
518 .await
519 .map_err(|status| NodeError::GrpcError {
520 error: status.to_string(),
521 })?
522 .into_inner();
523 let blob_stream = stream.map(|result| match result {
524 Ok(proto_blob) => BlobContent::try_from(proto_blob).map_err(NodeError::from),
525 Err(status) => Err(NodeError::GrpcError {
526 error: status.to_string(),
527 }),
528 });
529 Ok(Box::pin(blob_stream))
530 }
531
532 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
533 async fn download_pending_blob(
534 &self,
535 chain_id: ChainId,
536 blob_id: BlobId,
537 ) -> Result<BlobContent, NodeError> {
538 let req = (chain_id, blob_id);
539 client_delegate!(self, download_pending_blob, req)?.try_into()
540 }
541
542 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
543 async fn handle_pending_blob(
544 &self,
545 chain_id: ChainId,
546 blob: BlobContent,
547 ) -> Result<ChainInfoResponse, NodeError> {
548 let req = (chain_id, blob);
549 GrpcClient::try_into_chain_info(client_delegate!(self, handle_pending_blob, req)?)
550 }
551
552 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
553 async fn download_certificate(
554 &self,
555 hash: CryptoHash,
556 ) -> Result<ConfirmedBlockCertificate, NodeError> {
557 ConfirmedBlockCertificate::try_from(Certificate::try_from(client_delegate!(
558 self,
559 download_certificate,
560 hash
561 )?)?)
562 .map_err(|_| NodeError::UnexpectedCertificateValue)
563 }
564
565 #[instrument(target = "grpc_client", skip_all, err(level = Level::DEBUG), fields(address = self.address))]
566 async fn download_certificates(
567 &self,
568 hashes: Vec<CryptoHash>,
569 ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
570 let mut missing_hashes = hashes;
571 let mut certs_collected = Vec::with_capacity(missing_hashes.len());
572 while !missing_hashes.is_empty() {
573 let missing = missing_hashes.clone();
575 let mut received: Vec<_> = Vec::<Certificate>::try_from(client_delegate!(
576 self,
577 download_certificates,
578 missing
579 )?)?
580 .into_iter()
581 .map(|cert| {
582 ConfirmedBlockCertificate::try_from(cert)
583 .map_err(|_| NodeError::UnexpectedCertificateValue)
584 })
585 .collect::<Result<_, _>>()?;
586
587 if received.is_empty() {
589 break;
590 }
591
592 missing_hashes = missing_hashes[received.len()..].to_vec();
594 certs_collected.append(&mut received);
595 }
596 ensure!(
597 missing_hashes.is_empty(),
598 NodeError::MissingCertificates(missing_hashes)
599 );
600 Ok(certs_collected)
601 }
602
603 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
604 async fn download_certificates_by_heights(
605 &self,
606 chain_id: ChainId,
607 heights: Vec<BlockHeight>,
608 ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
609 let mut missing = heights.into_iter().collect::<BTreeSet<_>>();
610 let mut certs_collected = vec![];
611 while !missing.is_empty() {
612 let request = CertificatesByHeightRequest {
613 chain_id,
614 heights: missing.iter().copied().collect(),
615 };
616 let mut received: Vec<_> =
617 client_delegate!(self, download_raw_certificates_by_heights, request)?
618 .certificates
619 .into_iter()
620 .map(
621 |RawCertificate {
622 lite_certificate,
623 confirmed_block,
624 }|
625 -> Result<ConfirmedBlockCertificate, NodeError> {
626 let cert = bcs::from_bytes::<LiteCertificate>(&lite_certificate)
627 .map_err(|_| NodeError::UnexpectedCertificateValue)?;
628
629 let block = bcs::from_bytes::<ConfirmedBlock>(&confirmed_block)
630 .map_err(|_| NodeError::UnexpectedCertificateValue)?;
631
632 cert.into_confirmed_certificate(block)
633 .ok_or(NodeError::UnexpectedCertificateValue)
634 },
635 )
636 .collect::<Result<_, _>>()?;
637
638 if received.is_empty() {
639 break;
640 }
641
642 for cert in &received {
644 missing.remove(&cert.inner().height());
645 }
646 certs_collected.append(&mut received);
647 }
648 certs_collected.sort_by_key(|cert| cert.inner().height());
649 Ok(certs_collected)
650 }
651
652 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
653 async fn blob_last_used_by(&self, blob_id: BlobId) -> Result<CryptoHash, NodeError> {
654 Ok(client_delegate!(self, blob_last_used_by, blob_id)?.try_into()?)
655 }
656
657 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
658 async fn blob_last_used_by_certificate(
659 &self,
660 blob_id: BlobId,
661 ) -> Result<ConfirmedBlockCertificate, NodeError> {
662 Ok(client_delegate!(self, blob_last_used_by_certificate, blob_id)?.try_into()?)
663 }
664
665 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
666 async fn event_block_heights(
667 &self,
668 event_ids: Vec<EventId>,
669 ) -> Result<Vec<Option<BlockHeight>>, NodeError> {
670 let request = api::EventBlockHeightsRequest::from(event_ids);
671 Ok(client_delegate!(self, event_block_heights, request)?.try_into()?)
672 }
673
674 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
675 async fn missing_blob_ids(&self, blob_ids: Vec<BlobId>) -> Result<Vec<BlobId>, NodeError> {
676 Ok(client_delegate!(self, missing_blob_ids, blob_ids)?.try_into()?)
677 }
678
679 #[expect(
680 clippy::cast_possible_truncation,
681 reason = "shard counts are bounded by validator config and fit in usize on supported targets"
682 )]
683 #[instrument(target = "grpc_client", skip(self), err(level = Level::DEBUG), fields(address = self.address))]
684 async fn get_shard_info(
685 &self,
686 chain_id: ChainId,
687 ) -> Result<linera_core::data_types::ShardInfo, NodeError> {
688 let response = client_delegate!(self, get_shard_info, chain_id)?;
689 Ok(linera_core::data_types::ShardInfo {
690 shard_id: response.shard_id as usize,
691 total_shards: response.total_shards as usize,
692 })
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use tonic::{Code, Status};
699
700 use super::GrpcClient;
701
702 const PROXY_503: &str = "protocol error: received message with invalid compression flag: 10 \
706 (valid flags are 0 and 1) while receiving response with status: 503 Service Unavailable";
707
708 const PROXY_503_DECOMPRESS: &str = "Error decompressing: corrupt deflate stream, \
712 while receiving response with status: 503 Service Unavailable";
713
714 fn is_retryable(message: &str) -> bool {
715 GrpcClient::is_retryable(&Status::new(Code::Internal, message))
716 }
717
718 #[test]
719 fn proxy_server_errors_are_retryable() {
720 assert!(is_retryable(PROXY_503));
721 assert!(is_retryable(PROXY_503_DECOMPRESS));
722 for status in [
723 "500 Internal Server Error",
724 "502 Bad Gateway",
725 "504 Gateway Timeout",
726 ] {
727 let message = format!("while receiving response with status: {status}");
728 assert!(is_retryable(&message), "{status} should be retryable");
729 }
730 }
731
732 #[test]
733 fn proxy_client_errors_are_not_retryable() {
734 for status in ["400 Bad Request", "404 Not Found", "429 Too Many Requests"] {
735 let message = format!("while receiving response with status: {status}");
736 assert!(!is_retryable(&message), "{status} should not be retryable");
737 }
738 }
739
740 #[test]
741 fn unrelated_internal_errors_are_not_retryable() {
742 assert!(!is_retryable("something else went wrong"));
743 assert!(!is_retryable("the chain is at height 503"));
745 }
746
747 #[test]
748 fn connection_resets_are_retryable() {
749 assert!(is_retryable("h2 protocol error: stream closed"));
750 }
751}