1use dapi_grpc::mock::Mockable;
4use dapi_grpc::tonic::async_trait;
5#[cfg(not(target_arch = "wasm32"))]
6use dapi_grpc::tonic::transport::Certificate;
7use dapi_grpc::tonic::Status;
8use std::fmt::{Debug, Display};
9use std::time::Duration;
10use tracing::Instrument;
11
12use crate::address_list::AddressListError;
13use crate::connection_pool::{ConnectionPool, DEFAULT_POOL_CAPACITY};
14use crate::request_settings::AppliedRequestSettings;
15use crate::transport::{self, TransportError};
16use crate::{
17 transport::{TransportClient, TransportRequest},
18 AddressList, CanRetry, DapiRequestExecutor, ExecutionError, ExecutionResponse, ExecutionResult,
19 RequestSettings,
20};
21
22pub(crate) const MIN_RATE_LIMIT_BAN_SECS: u64 = 1;
28pub(crate) const MAX_RATE_LIMIT_BAN_SECS: u64 = 600;
32
33#[derive(Debug, thiserror::Error, Clone)]
35#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
36pub enum DapiClientError {
37 #[error("transport error: {0}")]
39 Transport(
40 #[cfg_attr(feature = "mocks", serde(with = "dapi_grpc::mock::serde_mockable"))]
41 TransportError,
42 ),
43 #[error("no available addresses to use")]
45 NoAvailableAddresses,
46 #[error("no available addresses to retry, last error: {0}")]
49 NoAvailableAddressesToRetry(
50 #[cfg_attr(feature = "mocks", serde(with = "dapi_grpc::mock::serde_mockable"))]
51 Box<TransportError>,
52 ),
53 #[error("address list error: {0}")]
55 AddressList(AddressListError),
56
57 #[cfg(feature = "mocks")]
58 #[error("mock error: {0}")]
59 Mock(#[from] crate::mock::MockError),
61}
62
63impl CanRetry for DapiClientError {
64 fn can_retry(&self) -> bool {
65 use DapiClientError::*;
66 match self {
67 NoAvailableAddresses => false,
68 NoAvailableAddressesToRetry(_) => false,
69 Transport(transport_error) => transport_error.can_retry(),
70 AddressList(_) => false,
71 #[cfg(feature = "mocks")]
72 Mock(_) => false,
73 }
74 }
75
76 fn is_no_available_addresses(&self) -> bool {
77 matches!(
78 self,
79 DapiClientError::NoAvailableAddresses | DapiClientError::NoAvailableAddressesToRetry(_)
80 )
81 }
82
83 fn rate_limit_ban_duration(&self) -> Option<Duration> {
84 match self {
85 DapiClientError::Transport(te) => te.rate_limit_ban_duration(),
86 _ => None,
87 }
88 }
89}
90
91impl Mockable for DapiClientError {
95 #[cfg(feature = "mocks")]
96 fn mock_serialize(&self) -> Option<Vec<u8>> {
97 Some(serde_json::to_vec(self).expect("serialize DAPI client error"))
98 }
99
100 #[cfg(feature = "mocks")]
101 fn mock_deserialize(data: &[u8]) -> Option<Self> {
102 Some(serde_json::from_slice(data).expect("deserialize DAPI client error"))
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct DapiClient {
109 address_list: AddressList,
110 settings: RequestSettings,
111 pool: ConnectionPool,
112 #[cfg(not(target_arch = "wasm32"))]
113 pub ca_certificate: Option<Certificate>,
115 #[cfg(feature = "dump")]
116 pub(crate) dump_dir: Option<std::path::PathBuf>,
117}
118
119impl DapiClient {
120 pub fn new(address_list: AddressList, settings: RequestSettings) -> Self {
125 let pool_capacity = (3 * address_list.len()).max(DEFAULT_POOL_CAPACITY);
128
129 Self {
130 address_list,
131 settings,
132 pool: ConnectionPool::new(pool_capacity),
133 #[cfg(feature = "dump")]
134 dump_dir: None,
135 #[cfg(not(target_arch = "wasm32"))]
136 ca_certificate: None,
137 }
138 }
139
140 #[cfg(not(target_arch = "wasm32"))]
149 pub fn with_ca_certificate(mut self, ca_cert: Certificate) -> Self {
150 self.ca_certificate = Some(ca_cert);
151
152 self
153 }
154
155 pub fn address_list(&self) -> &AddressList {
157 &self.address_list
158 }
159
160 pub fn get_live_addresses(&self) -> Vec<crate::Address> {
179 self.address_list.get_live_addresses()
180 }
181}
182
183pub fn update_address_ban_status<R, E>(
186 address_list: &AddressList,
187 result: &ExecutionResult<R, E>,
188 applied_settings: &AppliedRequestSettings,
189) where
190 E: CanRetry + Display + Debug,
191{
192 match &result {
193 Ok(response) => {
194 if address_list.is_banned(&response.address) {
196 if address_list.unban(&response.address) {
197 tracing::debug!(address = ?response.address, "unban successfully responded address {}", response.address);
198 } else {
199 tracing::debug!(
202 address = ?response.address,
203 "unable to unban address {} because it's not in the list anymore",
204 response.address
205 );
206 }
207 }
208 }
209 Err(error) => {
210 if error.can_retry() {
211 if let Some(address) = error.address.as_ref() {
212 if applied_settings.ban_failed_address {
213 let reason = Some(error.to_string());
214 let period_opt = error.rate_limit_ban_duration();
215 let banned = match period_opt {
216 Some(period) => address_list.ban_for(address, period, reason),
220 None => address_list.ban_with_reason(address, reason),
222 };
223 if banned {
224 if let Some(period) = period_opt {
225 tracing::debug!(
226 ?address,
227 ban_secs = period.as_secs(),
228 "rate-limited (ResourceExhausted): banning {address} for {}s (from RateLimit-Reset header)",
229 period.as_secs()
230 );
231 }
232 tracing::warn!(
233 ?address,
234 ?error,
235 "ban address {address} due to error: {error}"
236 );
237 } else {
238 tracing::debug!(
241 ?address,
242 ?error,
243 "unable to ban address {address} because it's not in the list anymore"
244 );
245 }
246 } else {
247 address_list.evict_from_rotation(address);
251 tracing::debug!(
252 ?error,
253 ?address,
254 "banning is disabled; evicted address {address} from rotation due to the error"
255 );
256 }
257 } else {
258 tracing::debug!(
259 ?error,
260 "we should ban an address due to the error but address is absent"
261 );
262 }
263 }
264 }
265 };
266}
267
268#[cfg(test)]
269#[allow(clippy::items_after_test_module)]
270mod tests {
271 use super::*;
272
273 fn mock_address() -> crate::Address {
274 "http://127.0.0.1:3000".parse().expect("valid address")
275 }
276
277 fn make_applied_settings(ban: bool) -> AppliedRequestSettings {
278 AppliedRequestSettings {
279 connect_timeout: None,
280 timeout: Duration::from_secs(10),
281 retries: 5,
282 ban_failed_address: ban,
283 max_decoding_message_size: None,
284 #[cfg(not(target_arch = "wasm32"))]
285 ca_certificate: None,
286 }
287 }
288
289 #[tokio::test]
290 async fn test_new_with_empty_address_list() {
291 let client = DapiClient::new(AddressList::new(), RequestSettings::default());
292 assert!(client.address_list().is_empty());
293
294 let request = dapi_grpc::platform::v0::GetIdentityRequest::default();
295 let err = client
296 .execute(request, RequestSettings::default())
297 .await
298 .expect_err("no addresses to execute the request on");
299 assert!(matches!(err.inner, DapiClientError::NoAvailableAddresses));
300
301 let mut address_list = client.address_list().clone();
303 assert!(address_list.add(mock_address()));
304 assert_eq!(client.get_live_addresses(), vec![mock_address()]);
305 }
307
308 #[test]
309 fn test_can_retry_no_available_addresses() {
310 let err = DapiClientError::NoAvailableAddresses;
311 assert!(!err.can_retry());
312 }
313
314 #[test]
315 fn test_can_retry_no_available_addresses_to_retry() {
316 let transport_err = TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("gone"));
317 let err = DapiClientError::NoAvailableAddressesToRetry(Box::new(transport_err));
318 assert!(!err.can_retry());
319 }
320
321 #[test]
322 fn test_can_retry_transport_retryable() {
323 let transport_err =
324 TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("temporary"));
325 let err = DapiClientError::Transport(transport_err);
326 assert!(err.can_retry());
327 }
328
329 #[test]
330 fn test_can_retry_transport_non_retryable() {
331 let transport_err = TransportError::Grpc(dapi_grpc::tonic::Status::not_found("permanent"));
332 let err = DapiClientError::Transport(transport_err);
333 assert!(!err.can_retry());
334 }
335
336 #[test]
337 fn test_can_retry_address_list_error() {
338 let err =
339 DapiClientError::AddressList(AddressListError::InvalidAddressUri("bad".to_string()));
340 assert!(!err.can_retry());
341 }
342
343 #[test]
347 fn test_rate_limit_ban_duration_header_parse() {
348 use dapi_grpc::tonic::metadata::MetadataValue;
349
350 let make_rl_status = |header: Option<&str>| -> dapi_grpc::tonic::Status {
352 let mut status = dapi_grpc::tonic::Status::resource_exhausted("429");
353 if let Some(v) = header {
354 status
355 .metadata_mut()
356 .insert("ratelimit-reset", MetadataValue::try_from(v).unwrap());
357 }
358 status
359 };
360
361 let s = make_rl_status(Some("45"));
363 let dur = TransportError::Grpc(s).rate_limit_ban_duration();
364 assert_eq!(dur, Some(Duration::from_secs(45)));
365
366 let s = make_rl_status(Some("9999"));
368 let dur = TransportError::Grpc(s).rate_limit_ban_duration();
369 assert_eq!(dur, Some(Duration::from_secs(MAX_RATE_LIMIT_BAN_SECS)));
370
371 let s = make_rl_status(Some("1"));
373 assert_eq!(
374 TransportError::Grpc(s).rate_limit_ban_duration(),
375 Some(Duration::from_secs(1))
376 );
377
378 let s = make_rl_status(Some("600"));
380 assert_eq!(
381 TransportError::Grpc(s).rate_limit_ban_duration(),
382 Some(Duration::from_secs(600))
383 );
384
385 let s = make_rl_status(Some("601"));
387 assert_eq!(
388 TransportError::Grpc(s).rate_limit_ban_duration(),
389 Some(Duration::from_secs(600))
390 );
391
392 let s = make_rl_status(Some("0"));
394 assert!(TransportError::Grpc(s).rate_limit_ban_duration().is_none());
395
396 let s = make_rl_status(Some("garbage"));
398 assert!(TransportError::Grpc(s).rate_limit_ban_duration().is_none());
399
400 let s = make_rl_status(None);
402 assert!(TransportError::Grpc(s).rate_limit_ban_duration().is_none());
403
404 let mut unavail = dapi_grpc::tonic::Status::unavailable("down");
406 unavail
407 .metadata_mut()
408 .insert("ratelimit-reset", MetadataValue::try_from("30").unwrap());
409 assert!(TransportError::Grpc(unavail)
410 .rate_limit_ban_duration()
411 .is_none());
412 }
413
414 #[test]
419 fn test_update_address_ban_status_rate_limit_ban_path() {
420 use dapi_grpc::tonic::metadata::MetadataValue;
421
422 let mut address_list = AddressList::new();
423 let addr = mock_address();
424 address_list.add(addr.clone());
425
426 let mut status = dapi_grpc::tonic::Status::resource_exhausted("429");
428 status
429 .metadata_mut()
430 .insert("ratelimit-reset", MetadataValue::try_from("45").unwrap());
431
432 let result: ExecutionResult<i32, DapiClientError> = Err(ExecutionError {
433 inner: DapiClientError::Transport(TransportError::Grpc(status)),
434 retries: 0,
435 address: Some(addr.clone()),
436 });
437 let before = chrono::Utc::now();
438 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
439 let after = chrono::Utc::now();
440
441 let info = address_list.ban_info();
442 let entry = info.iter().find(|i| i.uri == addr.to_string()).unwrap();
443
444 assert!(entry.banned, "rate-limited node must be banned");
446 assert_eq!(entry.ban_count, 1, "ban_count must be 1 after ban_for");
447 let until = entry.banned_until.expect("banned_until set");
448 let lo = (until - before).num_milliseconds() as f64 / 1000.0;
449 let hi = (until - after).num_milliseconds() as f64 / 1000.0;
450 assert!(
451 lo >= 44.9 && hi <= 45.1,
452 "ban window must be ~45 s, got lo={lo} hi={hi}"
453 );
454 }
455
456 #[test]
460 fn test_update_address_ban_status_rate_limit_no_header_uses_ladder() {
461 let mut address_list = AddressList::new();
462 let addr = mock_address();
463 address_list.add(addr.clone());
464
465 let result: ExecutionResult<i32, DapiClientError> = Err(ExecutionError {
466 inner: DapiClientError::Transport(TransportError::Grpc(
467 dapi_grpc::tonic::Status::resource_exhausted("429"),
468 )),
469 retries: 0,
470 address: Some(addr.clone()),
471 });
472 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
473
474 let info = address_list.ban_info();
476 let entry = info.iter().find(|i| i.uri == addr.to_string()).unwrap();
477 assert!(
478 entry.banned,
479 "node must be banned on ResourceExhausted without header"
480 );
481 assert_eq!(
482 entry.ban_count, 1,
483 "first health-ladder ban → ban_count = 1"
484 );
485 }
486
487 #[cfg(feature = "mocks")]
488 #[test]
489 fn test_can_retry_mock_error() {
490 let err = DapiClientError::Mock(crate::mock::MockError::MockExpectationNotFound(
491 "test".to_string(),
492 ));
493 assert!(!err.can_retry());
494 }
495
496 #[test]
497 fn test_is_no_available_addresses() {
498 assert!(DapiClientError::NoAvailableAddresses.is_no_available_addresses());
499
500 let transport_err = TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("gone"));
501 assert!(
502 DapiClientError::NoAvailableAddressesToRetry(Box::new(transport_err))
503 .is_no_available_addresses()
504 );
505
506 let transport_err =
507 TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("temporary"));
508 assert!(!DapiClientError::Transport(transport_err).is_no_available_addresses());
509 }
510
511 #[test]
512 fn test_update_address_ban_status_success_unbans() {
513 let mut address_list = AddressList::new();
514 let addr = mock_address();
515 address_list.add(addr.clone());
516 address_list.ban(&addr);
517 assert!(address_list.is_banned(&addr));
518
519 let result: ExecutionResult<i32, DapiClientError> = Ok(ExecutionResponse {
520 inner: 42,
521 retries: 0,
522 address: addr.clone(),
523 });
524
525 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
526
527 assert!(!address_list.is_banned(&addr));
528 }
529
530 #[test]
531 fn test_update_address_ban_status_success_on_unbanned_is_noop() {
532 let mut address_list = AddressList::new();
533 let addr = mock_address();
534 address_list.add(addr.clone());
535
536 let result: ExecutionResult<i32, DapiClientError> = Ok(ExecutionResponse {
537 inner: 42,
538 retries: 0,
539 address: addr.clone(),
540 });
541
542 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
544 assert!(!address_list.is_banned(&addr));
545 }
546
547 #[test]
548 fn test_update_address_ban_status_retryable_error_bans_address() {
549 let mut address_list = AddressList::new();
550 let addr = mock_address();
551 address_list.add(addr.clone());
552
553 let transport_err =
554 TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("temporary"));
555 let result: ExecutionResult<i32, DapiClientError> = Err(ExecutionError {
556 inner: DapiClientError::Transport(transport_err),
557 retries: 0,
558 address: Some(addr.clone()),
559 });
560
561 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
562 assert!(address_list.is_banned(&addr));
563
564 let info = address_list.ban_info();
567 assert_eq!(info.len(), 1);
568 let reason = info[0].reason.as_deref().expect("ban reason recorded");
569 assert!(
570 reason.contains("temporary"),
571 "ban reason should carry the underlying error, got: {reason}"
572 );
573 }
574
575 #[test]
576 fn test_update_address_ban_status_retryable_error_ban_disabled() {
577 let mut address_list = AddressList::new();
578 let addr = mock_address();
579 address_list.add(addr.clone());
580
581 let transport_err =
582 TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("temporary"));
583 let result: ExecutionResult<i32, DapiClientError> = Err(ExecutionError {
584 inner: DapiClientError::Transport(transport_err),
585 retries: 0,
586 address: Some(addr.clone()),
587 });
588
589 update_address_ban_status(&address_list, &result, &make_applied_settings(false));
590 assert!(!address_list.is_banned(&addr));
592 }
593
594 #[test]
595 fn test_update_address_ban_status_non_retryable_error_does_not_ban() {
596 let mut address_list = AddressList::new();
597 let addr = mock_address();
598 address_list.add(addr.clone());
599
600 let result: ExecutionResult<i32, DapiClientError> = Err(ExecutionError {
601 inner: DapiClientError::NoAvailableAddresses,
602 retries: 0,
603 address: Some(addr.clone()),
604 });
605
606 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
607 assert!(!address_list.is_banned(&addr));
608 }
609
610 #[test]
611 fn test_update_address_ban_status_retryable_error_no_address() {
612 let address_list = AddressList::new();
613
614 let transport_err =
615 TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("temporary"));
616 let result: ExecutionResult<i32, DapiClientError> = Err(ExecutionError {
617 inner: DapiClientError::Transport(transport_err),
618 retries: 0,
619 address: None,
620 });
621
622 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
624 }
625
626 #[test]
627 fn test_update_address_ban_status_unban_removed_address() {
628 let mut address_list = AddressList::new();
629 let addr = mock_address();
630 address_list.add(addr.clone());
631 address_list.ban(&addr);
632
633 address_list.remove(&addr);
635
636 let result: ExecutionResult<i32, DapiClientError> = Ok(ExecutionResponse {
637 inner: 42,
638 retries: 0,
639 address: addr.clone(),
640 });
641
642 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
644 }
645
646 #[test]
647 fn test_update_address_ban_status_ban_removed_address() {
648 let address_list = AddressList::new();
649 let addr = mock_address();
650
651 let transport_err =
652 TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("temporary"));
653 let result: ExecutionResult<i32, DapiClientError> = Err(ExecutionError {
654 inner: DapiClientError::Transport(transport_err),
655 retries: 0,
656 address: Some(addr),
657 });
658
659 update_address_ban_status(&address_list, &result, &make_applied_settings(true));
661 }
662
663 #[test]
664 fn test_dapi_client_new() {
665 let address_list: AddressList = "http://127.0.0.1:3000,http://127.0.0.1:3001"
666 .parse()
667 .unwrap();
668 let client = DapiClient::new(address_list, RequestSettings::default());
669 assert_eq!(client.address_list().len(), 2);
670 }
671
672 #[test]
673 fn test_dapi_client_get_live_addresses() {
674 let address_list: AddressList = "http://127.0.0.1:3000,http://127.0.0.1:3001"
675 .parse()
676 .unwrap();
677 let client = DapiClient::new(address_list, RequestSettings::default());
678 let live = client.get_live_addresses();
679 assert_eq!(live.len(), 2);
680 }
681
682 #[cfg(not(target_arch = "wasm32"))]
683 #[test]
684 fn test_dapi_client_with_ca_certificate() {
685 let address_list: AddressList = "http://127.0.0.1:3000".parse().unwrap();
686 let client = DapiClient::new(address_list, RequestSettings::default());
687 let cert = dapi_grpc::tonic::transport::Certificate::from_pem("fake-pem-data");
688 let client = client.with_ca_certificate(cert);
689 assert!(client.ca_certificate.is_some());
690 }
691
692 #[cfg(feature = "mocks")]
693 #[test]
694 fn test_dapi_client_error_mock_serialize_deserialize() {
695 use dapi_grpc::mock::Mockable;
696
697 let err = DapiClientError::NoAvailableAddresses;
698 let serialized = err.mock_serialize().expect("should serialize");
699 let deserialized =
700 DapiClientError::mock_deserialize(&serialized).expect("should deserialize");
701 assert!(matches!(
702 deserialized,
703 DapiClientError::NoAvailableAddresses
704 ));
705 }
706
707 #[cfg(feature = "mocks")]
708 #[test]
709 fn test_dapi_client_error_transport_mock_roundtrip() {
710 use dapi_grpc::mock::Mockable;
711
712 let transport_err = TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("test"));
713 let err = DapiClientError::Transport(transport_err);
714 let serialized = err.mock_serialize().expect("should serialize");
715 let deserialized =
716 DapiClientError::mock_deserialize(&serialized).expect("should deserialize");
717 assert!(matches!(deserialized, DapiClientError::Transport(_)));
718 }
719
720 #[test]
721 fn test_dapi_client_error_display() {
722 let err = DapiClientError::NoAvailableAddresses;
723 let display = format!("{}", err);
724 assert!(display.contains("no available addresses"));
725
726 let transport_err = TransportError::Grpc(dapi_grpc::tonic::Status::unavailable("gone"));
727 let err = DapiClientError::NoAvailableAddressesToRetry(Box::new(transport_err));
728 let display = format!("{}", err);
729 assert!(display.contains("no available addresses to retry"));
730
731 let err =
732 DapiClientError::AddressList(AddressListError::InvalidAddressUri("bad".to_string()));
733 let display = format!("{}", err);
734 assert!(display.contains("address list error"));
735 }
736
737 #[cfg(not(target_arch = "wasm32"))]
740 mod deadline_pool_eviction {
741 use super::*;
742 use crate::connection_pool::{PoolItem, PoolPrefix};
743 use crate::transport::{BoxFuture, PlatformGrpcClient};
744 use crate::Uri;
745 use dapi_grpc::tonic::transport::Channel;
746 use dapi_grpc::tonic::Code;
747 use std::sync::{Arc, Mutex};
748
749 struct PooledClient {
752 uri: Uri,
753 }
754
755 impl PooledClient {
756 fn pooled(
757 uri: Uri,
758 settings: Option<&AppliedRequestSettings>,
759 pool: &ConnectionPool,
760 ) -> Result<Self, TransportError> {
761 pool.get_or_create(PoolPrefix::Platform, &uri, settings, || {
762 Ok::<_, TransportError>(PoolItem::Platform(PlatformGrpcClient::new(
763 Channel::builder(uri.clone()).connect_lazy(),
764 )))
765 })?;
766 Ok(Self { uri })
767 }
768 }
769
770 impl TransportClient for PooledClient {
771 fn with_uri(uri: Uri, pool: &ConnectionPool) -> Result<Self, TransportError> {
772 Self::pooled(uri, None, pool)
773 }
774
775 fn with_uri_and_settings(
776 uri: Uri,
777 settings: &AppliedRequestSettings,
778 pool: &ConnectionPool,
779 ) -> Result<Self, TransportError> {
780 Self::pooled(uri, Some(settings), pool)
781 }
782 }
783
784 #[derive(Debug)]
785 struct Pong;
786
787 impl Mockable for Pong {}
788
789 #[derive(Clone, Debug, Default)]
792 struct StallFirstRequest {
793 stalled: Arc<Mutex<Option<Uri>>>,
794 }
795
796 impl Mockable for StallFirstRequest {}
797
798 impl TransportRequest for StallFirstRequest {
799 type Client = PooledClient;
800 type Response = Pong;
801
802 const SETTINGS_OVERRIDES: RequestSettings = RequestSettings::default();
803
804 fn method_name(&self) -> &'static str {
805 "stall_first"
806 }
807
808 fn execute_transport<'c>(
809 self,
810 client: &'c mut Self::Client,
811 _settings: &AppliedRequestSettings,
812 ) -> BoxFuture<'c, Result<Self::Response, TransportError>> {
813 let mut stalled = self.stalled.lock().expect("stall lock");
814 let target = stalled.get_or_insert_with(|| client.uri.clone());
815 if *target == client.uri {
816 Box::pin(futures::future::pending())
817 } else {
818 Box::pin(async { Ok(Pong) })
819 }
820 }
821 }
822
823 #[tokio::test(start_paused = true)]
824 async fn should_evict_the_pooled_connection_of_a_node_that_missed_its_deadline() {
825 let request = StallFirstRequest::default();
826 let client = DapiClient::new(
827 "http://127.0.0.1:10001,http://127.0.0.1:10002"
828 .parse()
829 .expect("valid address list"),
830 RequestSettings::default(),
831 );
832
833 let response = client
834 .execute(request.clone(), RequestSettings::default())
835 .await
836 .expect("the other node must answer");
837
838 let stalled = request
839 .stalled
840 .lock()
841 .expect("stall lock")
842 .clone()
843 .expect("a node stalled");
844 let settings = RequestSettings::default().finalize();
847 assert!(
848 client
849 .pool
850 .get(PoolPrefix::Platform, &stalled, Some(&settings))
851 .is_none(),
852 "the stalled node's connection must be evicted"
853 );
854 assert!(
855 client
856 .pool
857 .get(
858 PoolPrefix::Platform,
859 response.address.uri(),
860 Some(&settings)
861 )
862 .is_some(),
863 "the healthy node's connection must stay pooled"
864 );
865 }
866
867 #[derive(Clone, Debug)]
871 struct ReplaceConnectionThenStall {
872 pool: ConnectionPool,
873 }
874
875 impl Mockable for ReplaceConnectionThenStall {}
876
877 impl TransportRequest for ReplaceConnectionThenStall {
878 type Client = PooledClient;
879 type Response = Pong;
880
881 const SETTINGS_OVERRIDES: RequestSettings = RequestSettings::default();
882
883 fn method_name(&self) -> &'static str {
884 "replace_connection_then_stall"
885 }
886
887 fn execute_transport<'c>(
888 self,
889 client: &'c mut Self::Client,
890 settings: &AppliedRequestSettings,
891 ) -> BoxFuture<'c, Result<Self::Response, TransportError>> {
892 self.pool.put(
893 &client.uri,
894 Some(settings),
895 PoolItem::Platform(PlatformGrpcClient::new(
896 Channel::builder(client.uri.clone()).connect_lazy(),
897 )),
898 );
899 Box::pin(futures::future::pending())
900 }
901 }
902
903 #[tokio::test(start_paused = true)]
904 async fn should_keep_a_connection_pooled_after_the_attempt_took_its_own() {
905 let client = DapiClient::new(
906 "http://127.0.0.1:10001"
907 .parse()
908 .expect("valid address list"),
909 RequestSettings {
910 retries: Some(0),
911 ..RequestSettings::default()
912 },
913 );
914 let request = ReplaceConnectionThenStall {
915 pool: client.pool.clone(),
916 };
917
918 let error = client
919 .execute(request, RequestSettings::default())
920 .await
921 .expect_err("the only node never answers");
922
923 assert!(
924 matches!(
925 &error.inner,
926 DapiClientError::Transport(TransportError::Grpc(status))
927 if status.code() == Code::DeadlineExceeded
928 ),
929 "expected DeadlineExceeded, got {:?}",
930 error.inner
931 );
932 let uri = error.address.expect("the attempted node").uri().clone();
933 let settings = RequestSettings::default().finalize();
934 assert!(
935 client
936 .pool
937 .get(PoolPrefix::Platform, &uri, Some(&settings))
938 .is_some(),
939 "a connection pooled after the attempt took its own must stay pooled"
940 );
941 }
942
943 struct ReplacedWhileBuildingClient;
946
947 impl ReplacedWhileBuildingClient {
948 fn build(
949 uri: Uri,
950 settings: Option<&AppliedRequestSettings>,
951 pool: &ConnectionPool,
952 ) -> Result<(Self, u64), TransportError> {
953 let connect = || {
954 Ok::<_, TransportError>(PoolItem::Platform(PlatformGrpcClient::new(
955 Channel::builder(uri.clone()).connect_lazy(),
956 )))
957 };
958 let (_, generation) = pool.get_or_create_with_generation(
959 PoolPrefix::Platform,
960 &uri,
961 settings,
962 connect,
963 )?;
964 pool.put(&uri, settings, connect()?);
967 Ok((Self, generation))
968 }
969 }
970
971 impl TransportClient for ReplacedWhileBuildingClient {
972 fn with_uri(uri: Uri, pool: &ConnectionPool) -> Result<Self, TransportError> {
973 Self::build(uri, None, pool).map(|(client, _)| client)
974 }
975
976 fn with_uri_and_settings(
977 uri: Uri,
978 settings: &AppliedRequestSettings,
979 pool: &ConnectionPool,
980 ) -> Result<Self, TransportError> {
981 Self::build(uri, Some(settings), pool).map(|(client, _)| client)
982 }
983
984 fn with_uri_and_settings_and_generation(
985 uri: Uri,
986 settings: &AppliedRequestSettings,
987 pool: &ConnectionPool,
988 ) -> Result<(Self, u64), TransportError> {
989 Self::build(uri, Some(settings), pool)
990 }
991 }
992
993 #[derive(Clone, Debug)]
995 struct StallOnReplacedConnection;
996
997 impl Mockable for StallOnReplacedConnection {}
998
999 impl TransportRequest for StallOnReplacedConnection {
1000 type Client = ReplacedWhileBuildingClient;
1001 type Response = Pong;
1002
1003 const SETTINGS_OVERRIDES: RequestSettings = RequestSettings::default();
1004
1005 fn method_name(&self) -> &'static str {
1006 "stall_on_replaced_connection"
1007 }
1008
1009 fn execute_transport<'c>(
1010 self,
1011 _client: &'c mut Self::Client,
1012 _settings: &AppliedRequestSettings,
1013 ) -> BoxFuture<'c, Result<Self::Response, TransportError>> {
1014 Box::pin(futures::future::pending())
1015 }
1016 }
1017
1018 #[tokio::test(start_paused = true)]
1019 async fn should_keep_a_connection_pooled_while_the_client_was_being_built() {
1020 let client = DapiClient::new(
1021 "http://127.0.0.1:10001"
1022 .parse()
1023 .expect("valid address list"),
1024 RequestSettings {
1025 retries: Some(0),
1026 ..RequestSettings::default()
1027 },
1028 );
1029
1030 let error = client
1031 .execute(StallOnReplacedConnection, RequestSettings::default())
1032 .await
1033 .expect_err("the only node never answers");
1034
1035 assert!(
1036 matches!(
1037 &error.inner,
1038 DapiClientError::Transport(TransportError::Grpc(status))
1039 if status.code() == Code::DeadlineExceeded
1040 ),
1041 "expected DeadlineExceeded, got {:?}",
1042 error.inner
1043 );
1044 let uri = error.address.expect("the attempted node").uri().clone();
1045 let settings = RequestSettings::default().finalize();
1046 assert!(
1047 client
1048 .pool
1049 .get(PoolPrefix::Platform, &uri, Some(&settings))
1050 .is_some(),
1051 "a connection pooled while the attempt's client was being built must stay pooled"
1052 );
1053 }
1054 }
1055}
1056
1057#[async_trait]
1058impl DapiRequestExecutor for DapiClient {
1059 async fn execute<R>(
1061 &self,
1062 request: R,
1063 settings: RequestSettings,
1064 ) -> ExecutionResult<R::Response, DapiClientError>
1065 where
1066 R: TransportRequest + Mockable,
1067 R::Response: Mockable,
1068 TransportError: Mockable,
1069 {
1070 if let Some(refusal) = request.names_nothing() {
1073 return Err(ExecutionError {
1074 inner: DapiClientError::Transport(TransportError::Grpc(Status::invalid_argument(
1075 refusal,
1076 ))),
1077 retries: 0,
1078 address: None,
1079 });
1080 }
1081
1082 let applied_settings = self
1084 .settings
1085 .override_by(R::SETTINGS_OVERRIDES)
1086 .override_by(settings)
1087 .finalize();
1088 #[cfg(not(target_arch = "wasm32"))]
1089 let applied_settings = applied_settings.with_ca_certificate(self.ca_certificate.clone());
1090
1091 #[cfg(feature = "dump")]
1093 let dump_dir = self.dump_dir.clone();
1094 #[cfg(feature = "dump")]
1095 let dump_request = request.clone();
1096
1097 let max_retries = applied_settings.retries;
1098 let retry_delay = Duration::from_millis(10);
1099
1100 let mut retries: usize = 0;
1101 let mut last_transport_error: Option<TransportError> = None;
1103
1104 let result: ExecutionResult<R::Response, DapiClientError> = async {
1105 loop {
1106 let Some(address) = self.address_list.get_live_address() else {
1108 let error = if let Some(transport_error) = last_transport_error.take() {
1110 tracing::debug!(
1111 "no addresses available, returning last transport error"
1112 );
1113 DapiClientError::NoAvailableAddressesToRetry(Box::new(
1114 transport_error,
1115 ))
1116 } else {
1117 DapiClientError::NoAvailableAddresses
1118 };
1119
1120 return Err(ExecutionError {
1121 inner: error,
1122 retries,
1123 address: None,
1124 });
1125 };
1126
1127 tracing::trace!(
1130 target: "dapi_client::dispatch",
1131 ?address,
1132 method = request.method_name(),
1133 request_type = request.request_name(),
1134 "dispatching request to DAPI endpoint"
1135 );
1136 tracing::trace!(
1137 ?request,
1138 "calling {} with {} request",
1139 request.method_name(),
1140 request.request_name(),
1141 );
1142
1143 let transport_request = request.clone();
1144 let response_name = request.response_name();
1145
1146 let transport_client_result = R::Client::with_uri_and_settings_and_generation(
1148 address.uri().clone(),
1149 &applied_settings,
1150 &self.pool,
1151 );
1152
1153 let (mut transport_client, pool_generation) = match transport_client_result {
1158 Ok(client) => client,
1159 Err(transport_error) => {
1160 let can_retry_error = transport_error.can_retry();
1161
1162 let cloned_error = transport_error.clone();
1164
1165 let execution_error = ExecutionError {
1166 inner: DapiClientError::Transport(transport_error),
1167 retries,
1168 address: Some(address.clone()),
1169 };
1170
1171 update_address_ban_status::<R::Response, DapiClientError>(
1172 &self.address_list,
1173 &Err(execution_error.clone()),
1174 &applied_settings,
1175 );
1176
1177 if can_retry_error && retries < max_retries {
1178 last_transport_error = Some(cloned_error);
1180
1181 retries += 1;
1182 tracing::warn!(
1183 error = ?execution_error,
1184 "retrying error with sleeping {} secs",
1185 retry_delay.as_secs_f32()
1186 );
1187 transport::sleep(retry_delay).await;
1188 continue;
1189 }
1190
1191 return Err(execution_error);
1192 }
1193 };
1194
1195 let attempt = transport_request
1197 .execute_transport(&mut transport_client, &applied_settings)
1198 .instrument(tracing::trace_span!(
1199 "execute_request",
1200 ?address,
1201 settings = ?applied_settings,
1202 method = request.method_name(),
1203 ));
1204 #[cfg(not(target_arch = "wasm32"))]
1209 let result = match applied_settings.attempt_deadline() {
1210 Some(deadline) => match tokio::time::timeout(deadline, attempt).await {
1211 Ok(result) => result,
1212 Err(_) => {
1213 self.pool.remove_uri(address.uri(), pool_generation);
1214 Err(TransportError::Grpc(Status::deadline_exceeded(format!(
1215 "no complete response within {deadline:?}"
1216 ))))
1217 }
1218 },
1219 None => attempt.await,
1220 };
1221 #[cfg(target_arch = "wasm32")]
1222 let result = {
1223 let _ = pool_generation;
1224 attempt.await
1225 };
1226
1227 let execution_result = match result {
1228 Ok(response) => {
1229 tracing::trace!(response = ?response, "received {} response", response_name);
1230 Ok(ExecutionResponse {
1231 inner: response,
1232 retries,
1233 address: address.clone(),
1234 })
1235 }
1236 Err(transport_error) => {
1237 tracing::debug!(error = ?transport_error, "received error: {transport_error}");
1238 Err(ExecutionError {
1239 inner: DapiClientError::Transport(transport_error),
1240 retries,
1241 address: Some(address.clone()),
1242 })
1243 }
1244 };
1245
1246 update_address_ban_status::<R::Response, DapiClientError>(
1247 &self.address_list,
1248 &execution_result,
1249 &applied_settings,
1250 );
1251
1252 match execution_result {
1253 Ok(response) => return Ok(response),
1254 Err(error) => {
1255 if error.can_retry() && retries < max_retries {
1256 if let DapiClientError::Transport(ref te) = error.inner {
1258 last_transport_error = Some(te.clone());
1259 }
1260
1261 retries += 1;
1262 tracing::warn!(
1263 ?error,
1264 "retrying error with sleeping {} secs",
1265 retry_delay.as_secs_f32()
1266 );
1267 transport::sleep(retry_delay).await;
1268 continue;
1269 }
1270
1271 return Err(error);
1272 }
1273 }
1274 }
1275 }
1276 .instrument(tracing::info_span!("request routine"))
1277 .await;
1278
1279 if let Err(error) = &result {
1280 if !error.can_retry() {
1281 tracing::error!(?error, "request failed");
1282 }
1283 }
1284
1285 #[cfg(feature = "dump")]
1287 Self::dump_request_response(&dump_request, &result, dump_dir);
1288
1289 result
1290 }
1291}