1pub mod error;
2pub mod http;
3pub mod iroh;
4pub mod metrics;
5#[cfg(all(feature = "tor", not(target_family = "wasm")))]
6pub mod tor;
7pub mod ws;
8
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::fmt::{self, Debug};
11use std::net::SocketAddr;
12use std::pin::Pin;
13use std::str::FromStr as _;
14use std::sync::Arc;
15use std::time::Duration;
16
17use async_trait::async_trait;
18use fedimint_core::envs::{
19 FM_WS_API_CONNECT_OVERRIDES_ENV, is_running_in_test_env, parse_kv_list_from_env,
20};
21use fedimint_core::module::{ApiMethod, ApiRequestErased};
22use fedimint_core::util::backoff_util::{FibonacciBackoff, custom_backoff};
23use fedimint_core::util::{FmtCompact, SafeUrl};
24use fedimint_core::{apply, async_trait_maybe_send};
25use fedimint_logging::{LOG_CLIENT_NET_API, LOG_NET};
26use fedimint_metrics::HistogramExt as _;
27use reqwest::Method;
28use serde_json::Value;
29use tokio::sync::{OnceCell, SetOnce, broadcast, watch};
30use tracing::trace;
31
32use crate::error::{ConnectorError, ServerError};
33use crate::metrics::{CONNECTION_ATTEMPTS_TOTAL, CONNECTION_DURATION_SECONDS};
34use crate::ws::WebsocketConnector;
35
36const IROH_NEXT_PATH: &str = "/v1";
37
38pub fn iroh_next_endpoint_url(endpoint: &str) -> Result<SafeUrl, ConnectorError> {
44 let endpoint_id = iroh_next::EndpointId::from_str(endpoint).map_err(|source| {
45 ConnectorError::InvalidNodeId {
46 host: endpoint.to_owned(),
47 source: Box::new(source),
48 }
49 })?;
50 let url = format!("iroh://{endpoint_id}{IROH_NEXT_PATH}");
51 SafeUrl::parse(&url).map_err(|source| ConnectorError::InvalidUrl {
52 url,
53 source: Box::new(source),
54 })
55}
56
57fn is_iroh_next_endpoint_url(url: &SafeUrl) -> Result<bool, ConnectorError> {
58 match url.path() {
59 "" | "/" => Ok(false),
60 IROH_NEXT_PATH => Ok(true),
61 path => Err(ConnectorError::UnsupportedUrlPath {
62 path: path.to_owned(),
63 }),
64 }
65}
66
67fn preserve_iroh_next_marker(original: &SafeUrl, replacement: &SafeUrl) -> SafeUrl {
68 if original.scheme() == "iroh"
71 && original.path() == IROH_NEXT_PATH
72 && replacement.scheme() == "iroh"
73 {
74 let mut replacement = replacement.clone().to_unsafe();
75 replacement.set_path(IROH_NEXT_PATH);
76 replacement.into()
77 } else {
78 replacement.clone()
79 }
80}
81
82pub type ServerResult<T> = Result<T, ServerError>;
83
84type ConnectorInitFn = Arc<
86 dyn Fn() -> Pin<Box<dyn Future<Output = Result<DynConnector, ConnectorError>> + Send>>
87 + Send
88 + Sync,
89>;
90
91#[derive(Debug, Clone)]
96#[allow(clippy::struct_excessive_bools)] pub struct ConnectorRegistryBuilder {
98 connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
103
104 iroh_enable: bool,
106 iroh_dns: Option<SafeUrl>,
108 iroh_pkarr_dht: bool,
110 iroh_next: bool,
112
113 ws_enable: bool,
115 ws_force_tor: bool,
116
117 http_enable: bool,
119}
120
121impl ConnectorRegistryBuilder {
122 #[allow(clippy::unused_async)] pub async fn bind(self) -> ConnectorRegistry {
124 let iroh_next = self.iroh_next && self.iroh_enable;
125
126 let mut connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)> =
128 BTreeMap::new();
129
130 let path_change = Arc::new(watch::channel(0u64).0);
134
135 let builder_ws = self.clone();
137 let ws_connector_init = Arc::new(move || {
138 let builder = builder_ws.clone();
139 Box::pin(async move { builder.build_ws_connector().await })
140 as Pin<Box<dyn Future<Output = Result<DynConnector, ConnectorError>> + Send>>
141 });
142 connectors_lazy.insert("ws".into(), (ws_connector_init.clone(), OnceCell::new()));
143 connectors_lazy.insert("wss".into(), (ws_connector_init.clone(), OnceCell::new()));
144
145 let builder_iroh = self.clone();
147 let path_change_iroh = path_change.clone();
148 connectors_lazy.insert(
149 "iroh".into(),
150 (
151 Arc::new(move || {
152 let builder = builder_iroh.clone();
153 let path_change = path_change_iroh.clone();
154 Box::pin(async move { builder.build_iroh_connector(path_change).await })
155 as Pin<
156 Box<dyn Future<Output = Result<DynConnector, ConnectorError>> + Send>,
157 >
158 }),
159 OnceCell::new(),
160 ),
161 );
162
163 let builder_http = self.clone();
164 let http_connector_init = Arc::new(move || {
165 let builder = builder_http.clone();
166 Box::pin(async move { builder.build_http_connector() })
167 as Pin<Box<dyn Future<Output = Result<DynConnector, ConnectorError>> + Send>>
168 });
169
170 connectors_lazy.insert(
171 "http".into(),
172 (http_connector_init.clone(), OnceCell::new()),
173 );
174 connectors_lazy.insert(
175 "https".into(),
176 (http_connector_init.clone(), OnceCell::new()),
177 );
178
179 ConnectorRegistry {
180 inner: ConnectorRegistryInner {
181 connectors_lazy,
182 connection_overrides: self.connection_overrides,
183 initialized: SetOnce::new(),
184 path_change,
185 iroh_next,
186 }
187 .into(),
188 }
189 }
190
191 pub async fn build_iroh_connector(
192 &self,
193 path_change: Arc<watch::Sender<u64>>,
194 ) -> Result<DynConnector, ConnectorError> {
195 if !self.iroh_enable {
196 return Err(ConnectorError::NotEnabled { scheme: "iroh" });
197 }
198 let connector =
199 iroh::IrohConnector::new(self.iroh_dns.clone(), self.iroh_pkarr_dht, path_change)
200 .await?;
201
202 Ok(Arc::new(connector) as DynConnector)
203 }
204
205 pub async fn build_ws_connector(&self) -> Result<DynConnector, ConnectorError> {
206 if !self.ws_enable {
207 return Err(ConnectorError::NotEnabled { scheme: "ws" });
208 }
209
210 match self.ws_force_tor {
211 #[cfg(all(feature = "tor", not(target_family = "wasm")))]
212 true => {
213 use crate::tor::TorConnector;
214
215 Ok(Arc::new(TorConnector::bootstrap().await?) as DynConnector)
216 }
217
218 false => Ok(Arc::new(WebsocketConnector::new()) as DynConnector),
219 #[allow(unreachable_patterns)]
220 _ => Err(ConnectorError::TorNotCompiledIn),
221 }
222 }
223
224 pub fn build_http_connector(&self) -> Result<DynConnector, ConnectorError> {
225 if !self.http_enable {
226 return Err(ConnectorError::NotEnabled { scheme: "http" });
227 }
228
229 Ok(Arc::new(crate::http::HttpConnector::default()) as DynConnector)
230 }
231
232 pub fn iroh_pkarr_dht(self, enable: bool) -> Self {
233 Self {
234 iroh_pkarr_dht: enable,
235 ..self
236 }
237 }
238
239 pub fn iroh_next(self, enable: bool) -> Self {
242 Self {
243 iroh_next: enable,
244 ..self
245 }
246 }
247
248 pub fn ws_force_tor(self, enable: bool) -> Self {
249 Self {
250 ws_force_tor: enable,
251 ..self
252 }
253 }
254
255 pub fn http(self, enable: bool) -> Self {
256 Self {
257 http_enable: enable,
258 ..self
259 }
260 }
261
262 pub fn set_iroh_dns(self, url: SafeUrl) -> Self {
263 Self {
264 iroh_dns: Some(url),
265 ..self
266 }
267 }
268
269 pub fn with_env_var_overrides(mut self) -> Self {
271 for (k, v) in parse_kv_list_from_env::<_, SafeUrl>(FM_WS_API_CONNECT_OVERRIDES_ENV) {
273 self = self.with_connection_override(k, v);
274 }
275
276 if is_running_in_test_env() {
279 self.iroh_next = false;
280 }
281
282 self
283 }
284
285 pub fn with_connection_override(
286 mut self,
287 original_url: SafeUrl,
288 replacement_url: SafeUrl,
289 ) -> Self {
290 self.connection_overrides
291 .insert(original_url, replacement_url);
292 self
293 }
294}
295
296struct ConnectorRegistryInner {
298 connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)>,
300 connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
302 initialized: tokio::sync::SetOnce<()>,
307 path_change: Arc<watch::Sender<u64>>,
310 iroh_next: bool,
313}
314
315#[derive(Clone)]
328pub struct ConnectorRegistry {
329 inner: Arc<ConnectorRegistryInner>,
330}
331
332impl fmt::Debug for ConnectorRegistry {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 f.debug_struct("ConnectorRegistry")
335 .field("connectors_lazy", &self.inner.connectors_lazy.len())
336 .field("connection_overrides", &self.inner.connection_overrides)
337 .field("iroh_next", &self.inner.iroh_next)
338 .finish()
339 }
340}
341
342impl ConnectorRegistry {
343 pub fn iroh_next_enabled(&self) -> bool {
346 self.inner.iroh_next
347 }
348
349 pub fn build_from_client_defaults() -> ConnectorRegistryBuilder {
354 ConnectorRegistryBuilder {
355 iroh_enable: true,
356 iroh_dns: None,
357 iroh_pkarr_dht: false,
358 iroh_next: true,
359 ws_enable: true,
360 ws_force_tor: false,
361 http_enable: true,
362
363 connection_overrides: BTreeMap::default(),
364 }
365 }
366
367 pub fn build_from_server_defaults() -> ConnectorRegistryBuilder {
370 ConnectorRegistryBuilder {
371 iroh_enable: true,
372 iroh_dns: None,
373 iroh_pkarr_dht: true,
374 iroh_next: true,
375 ws_enable: true,
376 ws_force_tor: false,
377 http_enable: false,
378
379 connection_overrides: BTreeMap::default(),
380 }
381 }
382
383 pub fn build_from_testing_defaults() -> ConnectorRegistryBuilder {
386 ConnectorRegistryBuilder {
387 iroh_enable: true,
388 iroh_dns: None,
389 iroh_pkarr_dht: false,
390 iroh_next: false,
391 ws_enable: true,
392 ws_force_tor: false,
393 http_enable: true,
394
395 connection_overrides: BTreeMap::default(),
396 }
397 }
398
399 pub fn build_from_client_env() -> ConnectorRegistryBuilder {
402 Self::build_from_client_defaults().with_env_var_overrides()
403 }
404
405 pub fn build_from_server_env() -> ConnectorRegistryBuilder {
408 Self::build_from_server_defaults().with_env_var_overrides()
409 }
410
411 pub fn build_from_testing_env() -> ConnectorRegistryBuilder {
414 Self::build_from_testing_defaults().with_env_var_overrides()
415 }
416
417 pub async fn wait_for_initialized_connections(&self) {
419 self.inner.initialized.wait().await;
420 }
421
422 pub async fn connect_guardian(
427 &self,
428 url: &SafeUrl,
429 api_secret: Option<&str>,
430 ) -> ServerResult<DynGuaridianConnection> {
431 trace!(
432 target: LOG_NET,
433 %url,
434 "Connection requested to guardian"
435 );
436 let _ = self.inner.initialized.set(());
437
438 let replacement = self
439 .inner
440 .connection_overrides
441 .get(url)
442 .map(|replacement| preserve_iroh_next_marker(url, replacement));
443 let url = match replacement.as_ref() {
444 Some(replacement) => {
445 trace!(
446 target: LOG_NET,
447 original_url = %url,
448 replacement_url = %replacement,
449 "Using a connectivity override for connection"
450 );
451
452 replacement
453 }
454 None => url,
455 };
456
457 let scheme = url.scheme().to_string();
458
459 let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
460 return Err(ServerError::InvalidEndpoint(
461 format!(
462 "Unsupported scheme: {}; missing endpoint handler",
463 url.scheme()
464 )
465 .into(),
466 ));
467 };
468
469 let init_fn = connector_lazy.0.clone();
471
472 let timer = CONNECTION_DURATION_SECONDS
473 .with_label_values(&[&scheme])
474 .start_timer_ext();
475
476 let result = connector_lazy
477 .1
478 .get_or_try_init(|| async move { init_fn().await })
479 .await
480 .map_err(|e| {
481 ServerError::Transport(
484 format!("Connector failed to initialize: {}", e.fmt_compact()).into(),
485 )
486 })?
487 .connect_guardian(url, api_secret)
488 .await;
489
490 timer.observe_duration();
491
492 let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
493 CONNECTION_ATTEMPTS_TOTAL
494 .with_label_values(&[&scheme, &result_label])
495 .inc();
496
497 let conn = result.inspect_err(|err| {
498 trace!(
499 target: LOG_NET,
500 %url,
501 err = %err.fmt_compact(),
502 "Connection failed"
503 );
504 })?;
505
506 trace!(
507 target: LOG_NET,
508 %url,
509 "Connection returned"
510 );
511 Ok(conn)
512 }
513
514 pub async fn connect_gateway(
519 &self,
520 url: &SafeUrl,
521 ) -> Result<DynGatewayConnection, ConnectorError> {
522 trace!(
523 target: LOG_NET,
524 %url,
525 "Connection requested to gateway"
526 );
527 let _ = self.inner.initialized.set(());
528
529 let url = match self.inner.connection_overrides.get(url) {
530 Some(replacement) => {
531 trace!(
532 target: LOG_NET,
533 original_url = %url,
534 replacement_url = %replacement,
535 "Using a connectivity override for connection"
536 );
537
538 replacement
539 }
540 None => url,
541 };
542
543 let scheme = url.scheme().to_string();
544
545 let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
546 return Err(ConnectorError::UnsupportedScheme { scheme });
547 };
548
549 let init_fn = connector_lazy.0.clone();
551
552 let timer = CONNECTION_DURATION_SECONDS
553 .with_label_values(&[&scheme])
554 .start_timer_ext();
555
556 let result = connector_lazy
557 .1
558 .get_or_try_init(|| async move { init_fn().await })
559 .await?
560 .connect_gateway(url)
561 .await;
562
563 timer.observe_duration();
564
565 let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
566 CONNECTION_ATTEMPTS_TOTAL
567 .with_label_values(&[&scheme, &result_label])
568 .inc();
569
570 result
571 }
572
573 pub fn connectivity(&self, url: &SafeUrl) -> Connectivity {
579 let url = match self.inner.connection_overrides.get(url) {
580 Some(replacement) => replacement,
581 None => url,
582 };
583
584 let Some((_, connector_cell)) = self.inner.connectors_lazy.get(url.scheme()) else {
585 return Connectivity::Unknown;
586 };
587
588 match connector_cell.get() {
589 Some(connector) => connector.connectivity(url),
590 None => Connectivity::Unknown,
591 }
592 }
593
594 pub async fn iroh_peer_info(
597 &self,
598 url: &SafeUrl,
599 path_timeout: Duration,
600 ) -> ServerResult<Option<IrohPeerInfo>> {
601 let url = match self.inner.connection_overrides.get(url) {
602 Some(replacement) => replacement,
603 None => url,
604 };
605
606 let Some((init_fn, connector_cell)) = self.inner.connectors_lazy.get(url.scheme()) else {
607 return Ok(None);
608 };
609
610 let init_fn = init_fn.clone();
611 connector_cell
612 .get_or_try_init(|| async move { init_fn().await })
613 .await
614 .map_err(|e| {
615 ServerError::Transport(
618 format!("Connector failed to initialize: {}", e.fmt_compact()).into(),
619 )
620 })?
621 .iroh_peer_info(url, path_timeout)
622 .await
623 }
624
625 pub fn connectivity_change_notifier(&self) -> watch::Receiver<u64> {
634 self.inner.path_change.subscribe()
635 }
636}
637pub type DynConnector = Arc<dyn Connector>;
638
639#[async_trait]
640pub trait Connector: Send + Sync + 'static + Debug {
641 async fn connect_guardian(
642 &self,
643 url: &SafeUrl,
644 api_secret: Option<&str>,
645 ) -> ServerResult<DynGuaridianConnection>;
646
647 async fn connect_gateway(&self, url: &SafeUrl) -> Result<DynGatewayConnection, ConnectorError>;
648
649 fn connectivity(&self, url: &SafeUrl) -> Connectivity;
651
652 async fn iroh_peer_info(
654 &self,
655 _url: &SafeUrl,
656 _path_timeout: Duration,
657 ) -> ServerResult<Option<IrohPeerInfo>> {
658 Ok(None)
659 }
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
671pub enum Connectivity {
672 Direct,
673 Relay,
674 Mixed,
675 Tor,
676 Unknown,
677}
678
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
686pub enum PeerStatus {
687 Disconnected,
688 Connected(Connectivity),
689}
690
691#[derive(Debug, Clone, PartialEq, Eq)]
693pub struct IrohPeerInfo {
694 pub node_id: String,
695 pub connectivity: Connectivity,
696 pub direct_addr: Option<SocketAddr>,
697 pub known_direct_addrs: Vec<SocketAddr>,
698 pub relay_url: Option<String>,
699}
700
701#[apply(async_trait_maybe_send!)]
704pub trait IConnection: Debug + Send + Sync + 'static {
705 fn is_connected(&self) -> bool;
706
707 async fn await_disconnection(&self);
708}
709
710pub type DynGuaridianConnection = Arc<dyn IGuardianConnection>;
712
713#[async_trait]
715pub trait IGuardianConnection: IConnection + Debug + Send + Sync + 'static {
716 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value>;
717
718 fn into_dyn(self) -> DynGuaridianConnection
719 where
720 Self: Sized,
721 {
722 Arc::new(self)
723 }
724}
725
726pub type DynGatewayConnection = Arc<dyn IGatewayConnection>;
728
729#[apply(async_trait_maybe_send!)]
731pub trait IGatewayConnection: IConnection + Debug + Send + Sync + 'static {
732 async fn request(
733 &self,
734 password: Option<String>,
735 method: Method,
736 route: &str,
737 payload: Option<Value>,
738 ) -> ServerResult<Value>;
739
740 fn into_dyn(self) -> DynGatewayConnection
741 where
742 Self: Sized,
743 {
744 Arc::new(self)
745 }
746}
747
748#[derive(Debug)]
749pub struct ConnectionPool<T: IConnection + ?Sized> {
750 connectors: ConnectorRegistry,
752
753 active_connections: watch::Sender<BTreeSet<SafeUrl>>,
754
755 #[allow(clippy::type_complexity)]
761 connections: Arc<tokio::sync::Mutex<HashMap<SafeUrl, Arc<ConnectionState<T>>>>>,
762}
763
764impl<T: IConnection + ?Sized> Clone for ConnectionPool<T> {
765 fn clone(&self) -> Self {
766 Self {
767 connectors: self.connectors.clone(),
768 connections: self.connections.clone(),
769 active_connections: self.active_connections.clone(),
770 }
771 }
772}
773
774impl<T: IConnection + ?Sized> ConnectionPool<T> {
775 pub fn new(connectors: ConnectorRegistry) -> Self {
776 Self {
777 connectors,
778 connections: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
779 active_connections: watch::channel(BTreeSet::new()).0,
780 }
781 }
782
783 async fn get_or_init_pool_entry(&self, url: &SafeUrl) -> Arc<ConnectionState<T>> {
784 let mut pool_locked = self.connections.lock().await;
785 pool_locked
786 .entry(url.to_owned())
787 .and_modify(|entry_arc| {
788 if let Some(existing_conn) = entry_arc.connection.get()
794 && !existing_conn.is_connected()
795 {
796 trace!(
797 target: LOG_CLIENT_NET_API,
798 %url,
799 "Existing connection is disconnected, removing from pool"
800 );
801 self.active_connections.send_modify(|v| {
802 v.remove(url);
803 });
804 *entry_arc = Arc::new(ConnectionState::new_reconnecting());
805 }
806 })
807 .or_insert_with(|| Arc::new(ConnectionState::new_initial()))
808 .clone()
809 }
810
811 pub async fn get_or_create_connection<F, Fut>(
812 &self,
813 url: &SafeUrl,
814 api_secret: Option<&str>,
815 create_connection: F,
816 ) -> ServerResult<Arc<T>>
817 where
818 F: Fn(SafeUrl, Option<String>, ConnectorRegistry) -> Fut + Clone + Send + Sync + 'static,
819 Fut: Future<Output = ServerResult<Arc<T>>> + Send + 'static,
820 {
821 let pool_entry_arc = self.get_or_init_pool_entry(url).await;
822
823 let leader_tx = loop {
824 let mut leader_rx = {
825 let mut chan_locked = pool_entry_arc
826 .merge_connection_attempts_chan
827 .lock()
828 .expect("locking error");
829
830 if chan_locked.is_closed() {
831 let (leader_tx, leader_rx) = broadcast::channel(1);
832 *chan_locked = leader_rx;
833 break leader_tx;
836 }
837
838 chan_locked.resubscribe()
840 };
841
842 if let Ok(res) = leader_rx.recv().await {
843 match res {
844 Ok(o) => return Ok(o),
845 Err(err) => {
846 return Err(ServerError::Connection(err.into()));
847 }
848 }
849 }
850 };
851
852 let conn = pool_entry_arc
853 .connection
854 .get_or_try_init(|| async {
855 let retry_delay = pool_entry_arc.pre_reconnect_delay();
856 fedimint_core::runtime::sleep(retry_delay).await;
857 pool_entry_arc
858 .inner
859 .lock()
860 .expect("Locking failed")
861 .retry_at = None;
862
863 trace!(target: LOG_CLIENT_NET_API, %url, "Attempting to create a new connection");
864 let res = create_connection(
865 url.clone(),
866 api_secret.map(std::string::ToString::to_string),
867 self.connectors.clone(),
868 )
869 .await;
870
871 let _ = leader_tx.send(
876 res.as_ref()
877 .map(|o| o.clone())
878 .map_err(|err| err.fmt_compact().to_string()),
879 );
880
881 let conn = res?;
882
883 self.active_connections.send_modify(|v| {
884 v.insert(url.clone());
885 });
886
887 fedimint_core::runtime::spawn("connection disconnect watch", {
888 let conn = conn.clone();
889 let s = self.clone();
890 let url = url.clone();
891 async move {
892 conn.await_disconnection().await;
894 s.get_or_init_pool_entry(&url).await;
900 }
901 });
902
903 Ok(conn)
904 })
905 .await?;
906
907 trace!(target: LOG_CLIENT_NET_API, %url, "Connection ready");
908 Ok(conn.clone())
909 }
910 pub fn get_active_connection_receiver(&self) -> watch::Receiver<BTreeSet<SafeUrl>> {
912 self.active_connections.subscribe()
913 }
914
915 pub async fn wait_for_initialized_connections(&self) {
916 self.connectors.wait_for_initialized_connections().await
917 }
918
919 pub fn connectivity(&self, url: &SafeUrl) -> Connectivity {
921 self.connectors.connectivity(url)
922 }
923
924 pub fn connectivity_change_notifier(&self) -> watch::Receiver<u64> {
927 self.connectors.connectivity_change_notifier()
928 }
929}
930
931#[derive(Debug)]
934struct ConnectionStateInner {
935 fresh: bool,
936 backoff: FibonacciBackoff,
937 retry_at: Option<fedimint_core::runtime::Instant>,
938}
939
940#[derive(Debug)]
941pub struct ConnectionState<T: ?Sized> {
942 pub connection: tokio::sync::OnceCell<Arc<T>>,
944
945 merge_connection_attempts_chan:
949 std::sync::Mutex<broadcast::Receiver<std::result::Result<Arc<T>, String>>>,
950
951 inner: std::sync::Mutex<ConnectionStateInner>,
955}
956
957impl<T: ?Sized> ConnectionState<T> {
958 pub fn new_initial() -> Self {
960 Self {
961 connection: OnceCell::new(),
962 inner: std::sync::Mutex::new(ConnectionStateInner {
963 fresh: true,
964 retry_at: None,
965 backoff: custom_backoff(
966 Duration::from_millis(5),
968 Duration::from_secs(30),
969 None,
970 ),
971 }),
972 merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
973 }
974 }
975
976 pub fn new_reconnecting() -> Self {
979 Self {
980 connection: OnceCell::new(),
981 inner: std::sync::Mutex::new(ConnectionStateInner {
982 fresh: false,
984 retry_at: None,
985 backoff: custom_backoff(
986 Duration::from_millis(500),
988 Duration::from_secs(30),
989 None,
990 ),
991 }),
992 merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
993 }
994 }
995
996 pub fn pre_reconnect_delay(&self) -> Duration {
998 let mut backoff_locked = self.inner.lock().expect("Locking failed");
999 let now = fedimint_core::runtime::Instant::now();
1000 if let Some(retry_at) = backoff_locked.retry_at {
1001 return retry_at.saturating_duration_since(now);
1002 }
1003 let fresh = backoff_locked.fresh;
1004
1005 backoff_locked.fresh = false;
1006
1007 let delay = if fresh {
1008 Duration::default()
1009 } else {
1010 backoff_locked.backoff.next().expect("Keeps retrying")
1011 };
1012 backoff_locked.retry_at = Some(now + delay);
1013 delay
1014 }
1015}
1016
1017#[cfg(test)]
1018mod reconnect_tests;