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