Skip to main content

fedimint_connectors/
lib.rs

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
38/// Parse an advertised Iroh 1.0 endpoint ID into its API URL.
39///
40/// The `/v1` path is an internal transport-selection marker. It prevents the
41/// connector from attempting Iroh 0.35 against an Iroh 1.0-only identity,
42/// avoiding both an inappropriate connection attempt and its overhead.
43pub 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    // An Iroh-to-Iroh override changes the destination, not the selected wire
69    // version. Cross-protocol overrides deliberately replace the whole route.
70    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
84/// Type for connector initialization functions
85type ConnectorInitFn = Arc<
86    dyn Fn() -> Pin<Box<dyn Future<Output = Result<DynConnector, ConnectorError>> + Send>>
87        + Send
88        + Sync,
89>;
90
91/// Builder for [`ConnectorRegistry`]
92///
93/// See [`ConnectorRegistry::build_from_client_env`] and similar
94/// to create.
95#[derive(Debug, Clone)]
96#[allow(clippy::struct_excessive_bools)] // Shut up, Clippy
97pub struct ConnectorRegistryBuilder {
98    /// List of overrides to use when attempting to connect to given url
99    ///
100    /// This is useful for testing, or forcing non-default network
101    /// connectivity.
102    connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
103
104    /// Enable Iroh endpoints at all?
105    iroh_enable: bool,
106    /// Override the Iroh DNS server to use
107    iroh_dns: Option<SafeUrl>,
108    /// Enable Pkarr DHT discovery
109    iroh_pkarr_dht: bool,
110    /// Enable compatible iroh-next endpoint preference from guardian metadata
111    iroh_next: bool,
112
113    /// Enable Websocket API handling at all?
114    ws_enable: bool,
115    ws_force_tor: bool,
116
117    // Enable HTTP
118    http_enable: bool,
119}
120
121impl ConnectorRegistryBuilder {
122    #[allow(clippy::unused_async)] // Leave room for async in the future
123    pub async fn bind(self) -> ConnectorRegistry {
124        let iroh_next = self.iroh_next && self.iroh_enable;
125
126        // Create initialization functions for each connector type
127        let mut connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)> =
128            BTreeMap::new();
129
130        // Eagerly created so consumers can subscribe before the Iroh
131        // connector is lazily initialized. Only Iroh bumps it today
132        // (on transport-level path changes like relay → direct).
133        let path_change = Arc::new(watch::channel(0u64).0);
134
135        // WS connector init function
136        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        // Iroh connector init function
146        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    /// Enable use of compatible iroh-next endpoints advertised in guardian
240    /// metadata.
241    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    /// Apply overrides from env variables
270    pub fn with_env_var_overrides(mut self) -> Self {
271        // TODO: read rest of the env
272        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        // Disable iroh-next endpoint preference in test/devimint environments
277        // where iroh-next server endpoints are not running.
278        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
296/// Actual data shared between copies of [`ConnectorRegistry`] handle
297struct ConnectorRegistryInner {
298    /// Lazily initialized [`Connector`]s per protocol supported
299    connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)>,
300    /// Connection URL overrides for testing/custom routing
301    connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
302    /// Set on first connection attempt
303    ///
304    /// This is used for functionality that wants to avoid making
305    /// network connections if nothing else did network request.
306    initialized: tokio::sync::SetOnce<()>,
307    /// Ticks whenever a connector observes a transport-level path change
308    /// (e.g. iroh relay → direct). Only Iroh bumps this today.
309    path_change: Arc<watch::Sender<u64>>,
310    /// Whether compatible iroh-next endpoints advertised in guardian metadata
311    /// are used.
312    iroh_next: bool,
313}
314
315/// A set of available connectivity protocols a client can use to make
316/// network API requests (typically to federation).
317///
318/// Maps from connection URL schema to [`Connector`] to use to connect to it.
319///
320/// See [`ConnectorRegistry::build_from_client_env`] and similar
321/// to create.
322///
323/// [`ConnectorRegistry::connect_guardian`] is the main entry point for making
324/// mixed-networking stack connection.
325///
326/// Responsibilities:
327#[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    /// Whether compatible iroh-next endpoints advertised in guardian metadata
344    /// are used.
345    pub fn iroh_next_enabled(&self) -> bool {
346        self.inner.iroh_next
347    }
348
349    /// Create a builder with recommended defaults intended for client-side
350    /// usage
351    ///
352    /// In particular mobile devices are considered.
353    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    /// Create a builder with recommended defaults intended for the server-side
368    /// usage
369    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    /// Create a builder with recommended defaults intended for testing
384    /// usage
385    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    /// Like [`Self::build_from_client_defaults`] build will apply
400    /// environment-provided overrides.
401    pub fn build_from_client_env() -> ConnectorRegistryBuilder {
402        Self::build_from_client_defaults().with_env_var_overrides()
403    }
404
405    /// Like [`Self::build_from_server_defaults`] build will apply
406    /// environment-provided overrides.
407    pub fn build_from_server_env() -> ConnectorRegistryBuilder {
408        Self::build_from_server_defaults().with_env_var_overrides()
409    }
410
411    /// Like [`Self::build_from_testing_defaults`] build will apply
412    /// environment-provided overrides.
413    pub fn build_from_testing_env() -> ConnectorRegistryBuilder {
414        Self::build_from_testing_defaults().with_env_var_overrides()
415    }
416
417    /// Wait until some connections have been made
418    pub async fn wait_for_initialized_connections(&self) {
419        self.inner.initialized.wait().await;
420    }
421
422    /// Connect to a given `url` using matching [`Connector`]
423    ///
424    /// This is the main function consumed by the downstream use for making
425    /// connection.
426    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        // Clone the init function to use in the async block
470        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                // `Transport` is printed, not walked, by its consumers, so the whole chain
482                // goes into the text on purpose.
483                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    /// Connect to a given `url` using matching [`Connector`] to a gateway
515    ///
516    /// This is the main function consumed by the downstream use for making
517    /// connection.
518    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        // Clone the init function to use in the async block
550        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    /// Report how a connection to `url` is currently reaching its peer.
574    ///
575    /// Returns [`Connectivity::Unknown`] if no connector for the url's scheme
576    /// is registered, or if the matching connector has not been initialized
577    /// yet (i.e. no connection attempt has been made).
578    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    /// Return iroh-specific peer details if `url` is handled by the iroh
595    /// connector.
596    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                // `Transport` is printed, not walked, by its consumers, so the whole chain
616                // goes into the text on purpose.
617                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    /// Subscribe to transport-level connectivity changes across all
626    /// connectors managed by this registry.
627    ///
628    /// The receiver ticks whenever a connector observes a path change on
629    /// an existing connection (for example an iroh connection upgrading
630    /// from relay to direct). The carried `u64` is an opaque counter —
631    /// consumers should treat each update as a "re-read connectivity"
632    /// signal.
633    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    /// Report how a connection to `url` is currently reaching its peer.
650    fn connectivity(&self, url: &SafeUrl) -> Connectivity;
651
652    /// Return iroh-specific peer details if this connector supports them.
653    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/// How a connection is currently reaching its peer.
663///
664/// Transports without a relay concept (WS, HTTP) are always
665/// [`Connectivity::Direct`]. Tor-routed connections report
666/// [`Connectivity::Tor`]. Iroh connections may be [`Connectivity::Direct`]
667/// (peer-to-peer), [`Connectivity::Relay`] (routed through a relay
668/// server), or [`Connectivity::Mixed`] (both paths active); for Iroh this
669/// can change at runtime as hole-punching succeeds or falls back.
670#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
671pub enum Connectivity {
672    Direct,
673    Relay,
674    Mixed,
675    Tor,
676    Unknown,
677}
678
679/// Per-peer connection state reported by the federation API.
680///
681/// [`PeerStatus::Connected`] carries the current [`Connectivity`] of the
682/// active connection; for Iroh this reflects the path at the moment of the
683/// emission and may be stale until the next pool-level change (relay→direct
684/// upgrades on an existing connection are not yet streamed).
685#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
686pub enum PeerStatus {
687    Disconnected,
688    Connected(Connectivity),
689}
690
691/// Iroh-specific reachability details for a guardian endpoint.
692#[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/// Generic connection trait shared between [`IGuardianConnection`] and
702/// [`IGatewayConnection`]
703#[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
710/// A connection from api client to a federation guardian (type erased)
711pub type DynGuaridianConnection = Arc<dyn IGuardianConnection>;
712
713/// A connection from api client to a federation guardian
714#[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
726/// A connection from api client to a gateway (type erased)
727pub type DynGatewayConnection = Arc<dyn IGatewayConnection>;
728
729/// A connection from a client to a gateway
730#[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    /// Available connectors which we can make connections
751    connectors: ConnectorRegistry,
752
753    active_connections: watch::Sender<BTreeSet<SafeUrl>>,
754
755    /// Connection pool
756    ///
757    /// Every entry in this map will be created on demand and correspond to a
758    /// single outgoing connection to a certain URL that is in the process
759    /// of being established, or we already established.
760    #[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                // Check if existing connection is disconnected and reset the whole entry.
789                //
790                // This resets the state (like connectivity backoff), which is what we want.
791                // Since the (`OnceCell`) was already initialized, it means connection was
792                // successfully before, and disconnected afterwards.
793                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                    // whoever was trying to connect last time is gone
834                    // we're out of this lame loop for followers
835                    break leader_tx;
836                }
837
838                // lets piggyback on the existing leader
839                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                // If any other task was also waiting to connect, send them the connection
872                // result.
873                //
874                // Note: we want to send both Ok or Err, so `res?` is used only afterwards.
875                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                        // wait for this connection to disconnect
893                        conn.await_disconnection().await;
894                        // And afterwards, update `active_connections`.
895                        //
896                        // This will update the `active_connections` just like calling
897                        // `get_or_create_connection` normally do, but we will
898                        // not attempt to do anything with the result (i.e. try to connect).
899                        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    /// Get receiver for changes in the active connections
911    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    /// Report how a connection to `url` is currently reaching its peer.
920    pub fn connectivity(&self, url: &SafeUrl) -> Connectivity {
921        self.connectors.connectivity(url)
922    }
923
924    /// Subscribe to transport-level connectivity changes observed by
925    /// the connectors underlying this pool.
926    pub fn connectivity_change_notifier(&self) -> watch::Receiver<u64> {
927        self.connectors.connectivity_change_notifier()
928    }
929}
930
931/// Inner part of [`ConnectionState`] preserving state between attempts to
932/// initialize [`ConnectionState::connection`]
933#[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    /// Connection we are trying to or already established
943    pub connection: tokio::sync::OnceCell<Arc<T>>,
944
945    /// When tasks attempt to connect at the same time,
946    /// this is the receiving end of the channel where
947    /// the "leader" sends a result.
948    merge_connection_attempts_chan:
949        std::sync::Mutex<broadcast::Receiver<std::result::Result<Arc<T>, String>>>,
950
951    /// State that technically is protected every time by
952    /// the serialization of `OnceCell::get_or_try_init`, but
953    /// for Rust purposes needs to be locked.
954    inner: std::sync::Mutex<ConnectionStateInner>,
955}
956
957impl<T: ?Sized> ConnectionState<T> {
958    /// Create a new connection state for a first time connection
959    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                    // First time connections start quick
967                    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    /// Create a new connection state for a connection that already failed, and
977    /// is being reset
978    pub fn new_reconnecting() -> Self {
979        Self {
980            connection: OnceCell::new(),
981            inner: std::sync::Mutex::new(ConnectionStateInner {
982                // set the attempts to 1, indicating that
983                fresh: false,
984                retry_at: None,
985                backoff: custom_backoff(
986                    // Connections after a disconnect start with some minimum delay
987                    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    /// Return the remaining wait, preserving its deadline across cancellation.
997    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;