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 anyhow::{Context as _, anyhow, bail};
18use async_trait::async_trait;
19use fedimint_core::envs::{
20    FM_WS_API_CONNECT_OVERRIDES_ENV, is_running_in_test_env, parse_kv_list_from_env,
21};
22use fedimint_core::module::{ApiMethod, ApiRequestErased};
23use fedimint_core::util::backoff_util::{FibonacciBackoff, custom_backoff};
24use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl};
25use fedimint_core::{apply, async_trait_maybe_send};
26use fedimint_logging::{LOG_CLIENT_NET_API, LOG_NET};
27use fedimint_metrics::HistogramExt as _;
28use reqwest::Method;
29use serde_json::Value;
30use tokio::sync::{OnceCell, SetOnce, broadcast, watch};
31use tracing::trace;
32
33use crate::error::ServerError;
34use crate::metrics::{CONNECTION_ATTEMPTS_TOTAL, CONNECTION_DURATION_SECONDS};
35use crate::ws::WebsocketConnector;
36
37const IROH_NEXT_PATH: &str = "/v1";
38
39/// Parse an advertised Iroh 1.0 endpoint ID into its API URL.
40///
41/// The `/v1` path is an internal transport-selection marker. It prevents the
42/// connector from attempting Iroh 0.35 against an Iroh 1.0-only identity,
43/// avoiding both an inappropriate connection attempt and its overhead.
44pub fn iroh_next_endpoint_url(endpoint: &str) -> anyhow::Result<SafeUrl> {
45    let endpoint_id =
46        iroh_next::EndpointId::from_str(endpoint).context("Invalid Iroh 1.0 endpoint ID")?;
47    SafeUrl::parse(&format!("iroh://{endpoint_id}{IROH_NEXT_PATH}"))
48        .context("Invalid Iroh 1.0 endpoint URL")
49}
50
51fn is_iroh_next_endpoint_url(url: &SafeUrl) -> anyhow::Result<bool> {
52    match url.path() {
53        "" | "/" => Ok(false),
54        IROH_NEXT_PATH => Ok(true),
55        path => bail!("Unsupported Iroh API URL path: {path}"),
56    }
57}
58
59fn preserve_iroh_next_marker(original: &SafeUrl, replacement: &SafeUrl) -> SafeUrl {
60    // An Iroh-to-Iroh override changes the destination, not the selected wire
61    // version. Cross-protocol overrides deliberately replace the whole route.
62    if original.scheme() == "iroh"
63        && original.path() == IROH_NEXT_PATH
64        && replacement.scheme() == "iroh"
65    {
66        let mut replacement = replacement.clone().to_unsafe();
67        replacement.set_path(IROH_NEXT_PATH);
68        replacement.into()
69    } else {
70        replacement.clone()
71    }
72}
73
74pub type ServerResult<T> = Result<T, ServerError>;
75
76/// Type for connector initialization functions
77type ConnectorInitFn = Arc<
78    dyn Fn() -> Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>> + Send + Sync,
79>;
80
81/// Builder for [`ConnectorRegistry`]
82///
83/// See [`ConnectorRegistry::build_from_client_env`] and similar
84/// to create.
85#[derive(Debug, Clone)]
86#[allow(clippy::struct_excessive_bools)] // Shut up, Clippy
87pub struct ConnectorRegistryBuilder {
88    /// List of overrides to use when attempting to connect to given url
89    ///
90    /// This is useful for testing, or forcing non-default network
91    /// connectivity.
92    connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
93
94    /// Enable Iroh endpoints at all?
95    iroh_enable: bool,
96    /// Override the Iroh DNS server to use
97    iroh_dns: Option<SafeUrl>,
98    /// Enable Pkarr DHT discovery
99    iroh_pkarr_dht: bool,
100    /// Enable compatible iroh-next endpoint preference from guardian metadata
101    iroh_next: bool,
102
103    /// Enable Websocket API handling at all?
104    ws_enable: bool,
105    ws_force_tor: bool,
106
107    // Enable HTTP
108    http_enable: bool,
109}
110
111impl ConnectorRegistryBuilder {
112    #[allow(clippy::unused_async)] // Leave room for async in the future
113    pub async fn bind(self) -> anyhow::Result<ConnectorRegistry> {
114        let iroh_next = self.iroh_next && self.iroh_enable;
115
116        // Create initialization functions for each connector type
117        let mut connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)> =
118            BTreeMap::new();
119
120        // Eagerly created so consumers can subscribe before the Iroh
121        // connector is lazily initialized. Only Iroh bumps it today
122        // (on transport-level path changes like relay → direct).
123        let path_change = Arc::new(watch::channel(0u64).0);
124
125        // WS connector init function
126        let builder_ws = self.clone();
127        let ws_connector_init = Arc::new(move || {
128            let builder = builder_ws.clone();
129            Box::pin(async move { builder.build_ws_connector().await })
130                as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
131        });
132        connectors_lazy.insert("ws".into(), (ws_connector_init.clone(), OnceCell::new()));
133        connectors_lazy.insert("wss".into(), (ws_connector_init.clone(), OnceCell::new()));
134
135        // Iroh connector init function
136        let builder_iroh = self.clone();
137        let path_change_iroh = path_change.clone();
138        connectors_lazy.insert(
139            "iroh".into(),
140            (
141                Arc::new(move || {
142                    let builder = builder_iroh.clone();
143                    let path_change = path_change_iroh.clone();
144                    Box::pin(async move { builder.build_iroh_connector(path_change).await })
145                        as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
146                }),
147                OnceCell::new(),
148            ),
149        );
150
151        let builder_http = self.clone();
152        let http_connector_init = Arc::new(move || {
153            let builder = builder_http.clone();
154            Box::pin(async move { builder.build_http_connector() })
155                as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
156        });
157
158        connectors_lazy.insert(
159            "http".into(),
160            (http_connector_init.clone(), OnceCell::new()),
161        );
162        connectors_lazy.insert(
163            "https".into(),
164            (http_connector_init.clone(), OnceCell::new()),
165        );
166
167        Ok(ConnectorRegistry {
168            inner: ConnectorRegistryInner {
169                connectors_lazy,
170                connection_overrides: self.connection_overrides,
171                initialized: SetOnce::new(),
172                path_change,
173                iroh_next,
174            }
175            .into(),
176        })
177    }
178
179    pub async fn build_iroh_connector(
180        &self,
181        path_change: Arc<watch::Sender<u64>>,
182    ) -> anyhow::Result<DynConnector> {
183        if !self.iroh_enable {
184            bail!("Iroh connector not enabled");
185        }
186        Ok(Arc::new(
187            iroh::IrohConnector::new(self.iroh_dns.clone(), self.iroh_pkarr_dht, path_change)
188                .await?,
189        ) as DynConnector)
190    }
191
192    pub async fn build_ws_connector(&self) -> anyhow::Result<DynConnector> {
193        if !self.ws_enable {
194            bail!("Websocket connector not enabled");
195        }
196
197        match self.ws_force_tor {
198            #[cfg(all(feature = "tor", not(target_family = "wasm")))]
199            true => {
200                use crate::tor::TorConnector;
201
202                Ok(Arc::new(TorConnector::bootstrap().await?) as DynConnector)
203            }
204
205            false => Ok(Arc::new(WebsocketConnector::new()) as DynConnector),
206            #[allow(unreachable_patterns)]
207            _ => bail!("Tor requested, but not support not compiled in"),
208        }
209    }
210
211    pub fn build_http_connector(&self) -> anyhow::Result<DynConnector> {
212        if !self.http_enable {
213            bail!("Http connector not enabled");
214        }
215
216        Ok(Arc::new(crate::http::HttpConnector::default()) as DynConnector)
217    }
218
219    pub fn iroh_pkarr_dht(self, enable: bool) -> Self {
220        Self {
221            iroh_pkarr_dht: enable,
222            ..self
223        }
224    }
225
226    /// Enable use of compatible iroh-next endpoints advertised in guardian
227    /// metadata.
228    pub fn iroh_next(self, enable: bool) -> Self {
229        Self {
230            iroh_next: enable,
231            ..self
232        }
233    }
234
235    pub fn ws_force_tor(self, enable: bool) -> Self {
236        Self {
237            ws_force_tor: enable,
238            ..self
239        }
240    }
241
242    pub fn http(self, enable: bool) -> Self {
243        Self {
244            http_enable: enable,
245            ..self
246        }
247    }
248
249    pub fn set_iroh_dns(self, url: SafeUrl) -> Self {
250        Self {
251            iroh_dns: Some(url),
252            ..self
253        }
254    }
255
256    /// Apply overrides from env variables
257    pub fn with_env_var_overrides(mut self) -> anyhow::Result<Self> {
258        // TODO: read rest of the env
259        for (k, v) in parse_kv_list_from_env::<_, SafeUrl>(FM_WS_API_CONNECT_OVERRIDES_ENV)? {
260            self = self.with_connection_override(k, v);
261        }
262
263        // Disable iroh-next endpoint preference in test/devimint environments
264        // where iroh-next server endpoints are not running.
265        if is_running_in_test_env() {
266            self.iroh_next = false;
267        }
268
269        Ok(Self { ..self })
270    }
271
272    pub fn with_connection_override(
273        mut self,
274        original_url: SafeUrl,
275        replacement_url: SafeUrl,
276    ) -> Self {
277        self.connection_overrides
278            .insert(original_url, replacement_url);
279        self
280    }
281}
282
283/// Actual data shared between copies of [`ConnectorRegistry`] handle
284struct ConnectorRegistryInner {
285    /// Lazily initialized [`Connector`]s per protocol supported
286    connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)>,
287    /// Connection URL overrides for testing/custom routing
288    connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
289    /// Set on first connection attempt
290    ///
291    /// This is used for functionality that wants to avoid making
292    /// network connections if nothing else did network request.
293    initialized: tokio::sync::SetOnce<()>,
294    /// Ticks whenever a connector observes a transport-level path change
295    /// (e.g. iroh relay → direct). Only Iroh bumps this today.
296    path_change: Arc<watch::Sender<u64>>,
297    /// Whether compatible iroh-next endpoints advertised in guardian metadata
298    /// are used.
299    iroh_next: bool,
300}
301
302/// A set of available connectivity protocols a client can use to make
303/// network API requests (typically to federation).
304///
305/// Maps from connection URL schema to [`Connector`] to use to connect to it.
306///
307/// See [`ConnectorRegistry::build_from_client_env`] and similar
308/// to create.
309///
310/// [`ConnectorRegistry::connect_guardian`] is the main entry point for making
311/// mixed-networking stack connection.
312///
313/// Responsibilities:
314#[derive(Clone)]
315pub struct ConnectorRegistry {
316    inner: Arc<ConnectorRegistryInner>,
317}
318
319impl fmt::Debug for ConnectorRegistry {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        f.debug_struct("ConnectorRegistry")
322            .field("connectors_lazy", &self.inner.connectors_lazy.len())
323            .field("connection_overrides", &self.inner.connection_overrides)
324            .field("iroh_next", &self.inner.iroh_next)
325            .finish()
326    }
327}
328
329impl ConnectorRegistry {
330    /// Whether compatible iroh-next endpoints advertised in guardian metadata
331    /// are used.
332    pub fn iroh_next_enabled(&self) -> bool {
333        self.inner.iroh_next
334    }
335
336    /// Create a builder with recommended defaults intended for client-side
337    /// usage
338    ///
339    /// In particular mobile devices are considered.
340    pub fn build_from_client_defaults() -> ConnectorRegistryBuilder {
341        ConnectorRegistryBuilder {
342            iroh_enable: true,
343            iroh_dns: None,
344            iroh_pkarr_dht: false,
345            iroh_next: true,
346            ws_enable: true,
347            ws_force_tor: false,
348            http_enable: true,
349
350            connection_overrides: BTreeMap::default(),
351        }
352    }
353
354    /// Create a builder with recommended defaults intended for the server-side
355    /// usage
356    pub fn build_from_server_defaults() -> ConnectorRegistryBuilder {
357        ConnectorRegistryBuilder {
358            iroh_enable: true,
359            iroh_dns: None,
360            iroh_pkarr_dht: true,
361            iroh_next: true,
362            ws_enable: true,
363            ws_force_tor: false,
364            http_enable: false,
365
366            connection_overrides: BTreeMap::default(),
367        }
368    }
369
370    /// Create a builder with recommended defaults intended for testing
371    /// usage
372    pub fn build_from_testing_defaults() -> ConnectorRegistryBuilder {
373        ConnectorRegistryBuilder {
374            iroh_enable: true,
375            iroh_dns: None,
376            iroh_pkarr_dht: false,
377            iroh_next: false,
378            ws_enable: true,
379            ws_force_tor: false,
380            http_enable: true,
381
382            connection_overrides: BTreeMap::default(),
383        }
384    }
385
386    /// Like [`Self::build_from_client_defaults`] build will apply
387    /// environment-provided overrides.
388    pub fn build_from_client_env() -> anyhow::Result<ConnectorRegistryBuilder> {
389        let builder = Self::build_from_client_defaults().with_env_var_overrides()?;
390        Ok(builder)
391    }
392
393    /// Like [`Self::build_from_server_defaults`] build will apply
394    /// environment-provided overrides.
395    pub fn build_from_server_env() -> anyhow::Result<ConnectorRegistryBuilder> {
396        let builder = Self::build_from_server_defaults().with_env_var_overrides()?;
397        Ok(builder)
398    }
399
400    /// Like [`Self::build_from_testing_defaults`] build will apply
401    /// environment-provided overrides.
402    pub fn build_from_testing_env() -> anyhow::Result<ConnectorRegistryBuilder> {
403        let builder = Self::build_from_testing_defaults().with_env_var_overrides()?;
404        Ok(builder)
405    }
406
407    /// Wait until some connections have been made
408    pub async fn wait_for_initialized_connections(&self) {
409        self.inner.initialized.wait().await;
410    }
411
412    /// Connect to a given `url` using matching [`Connector`]
413    ///
414    /// This is the main function consumed by the downstream use for making
415    /// connection.
416    pub async fn connect_guardian(
417        &self,
418        url: &SafeUrl,
419        api_secret: Option<&str>,
420    ) -> ServerResult<DynGuaridianConnection> {
421        trace!(
422            target: LOG_NET,
423            %url,
424            "Connection requested to guardian"
425        );
426        let _ = self.inner.initialized.set(());
427
428        let replacement = self
429            .inner
430            .connection_overrides
431            .get(url)
432            .map(|replacement| preserve_iroh_next_marker(url, replacement));
433        let url = match replacement.as_ref() {
434            Some(replacement) => {
435                trace!(
436                    target: LOG_NET,
437                    original_url = %url,
438                    replacement_url = %replacement,
439                    "Using a connectivity override for connection"
440                );
441
442                replacement
443            }
444            None => url,
445        };
446
447        let scheme = url.scheme().to_string();
448
449        let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
450            return Err(ServerError::InvalidEndpoint(anyhow!(
451                "Unsupported scheme: {}; missing endpoint handler",
452                url.scheme()
453            )));
454        };
455
456        // Clone the init function to use in the async block
457        let init_fn = connector_lazy.0.clone();
458
459        let timer = CONNECTION_DURATION_SECONDS
460            .with_label_values(&[&scheme])
461            .start_timer_ext();
462
463        let result = connector_lazy
464            .1
465            .get_or_try_init(|| async move { init_fn().await })
466            .await
467            .map_err(|e| {
468                ServerError::Transport(anyhow!(
469                    "Connector failed to initialize: {}",
470                    e.fmt_compact_anyhow()
471                ))
472            })?
473            .connect_guardian(url, api_secret)
474            .await;
475
476        timer.observe_duration();
477
478        let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
479        CONNECTION_ATTEMPTS_TOTAL
480            .with_label_values(&[&scheme, &result_label])
481            .inc();
482
483        let conn = result.inspect_err(|err| {
484            trace!(
485                target: LOG_NET,
486                %url,
487                err = %err.fmt_compact(),
488                "Connection failed"
489            );
490        })?;
491
492        trace!(
493            target: LOG_NET,
494            %url,
495            "Connection returned"
496        );
497        Ok(conn)
498    }
499
500    /// Connect to a given `url` using matching [`Connector`] to a gateway
501    ///
502    /// This is the main function consumed by the downstream use for making
503    /// connection.
504    pub async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection> {
505        trace!(
506            target: LOG_NET,
507            %url,
508            "Connection requested to gateway"
509        );
510        let _ = self.inner.initialized.set(());
511
512        let url = match self.inner.connection_overrides.get(url) {
513            Some(replacement) => {
514                trace!(
515                    target: LOG_NET,
516                    original_url = %url,
517                    replacement_url = %replacement,
518                    "Using a connectivity override for connection"
519                );
520
521                replacement
522            }
523            None => url,
524        };
525
526        let scheme = url.scheme().to_string();
527
528        let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
529            return Err(anyhow!(
530                "Unsupported scheme: {}; missing endpoint handler",
531                url.scheme()
532            ));
533        };
534
535        // Clone the init function to use in the async block
536        let init_fn = connector_lazy.0.clone();
537
538        let timer = CONNECTION_DURATION_SECONDS
539            .with_label_values(&[&scheme])
540            .start_timer_ext();
541
542        let result = connector_lazy
543            .1
544            .get_or_try_init(|| async move { init_fn().await })
545            .await
546            .map_err(|e| {
547                ServerError::Transport(anyhow!(
548                    "Connector failed to initialize: {}",
549                    e.fmt_compact_anyhow()
550                ))
551            })?
552            .connect_gateway(url)
553            .await;
554
555        timer.observe_duration();
556
557        let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
558        CONNECTION_ATTEMPTS_TOTAL
559            .with_label_values(&[&scheme, &result_label])
560            .inc();
561
562        result
563    }
564
565    /// Report how a connection to `url` is currently reaching its peer.
566    ///
567    /// Returns [`Connectivity::Unknown`] if no connector for the url's scheme
568    /// is registered, or if the matching connector has not been initialized
569    /// yet (i.e. no connection attempt has been made).
570    pub fn connectivity(&self, url: &SafeUrl) -> Connectivity {
571        let url = match self.inner.connection_overrides.get(url) {
572            Some(replacement) => replacement,
573            None => url,
574        };
575
576        let Some((_, connector_cell)) = self.inner.connectors_lazy.get(url.scheme()) else {
577            return Connectivity::Unknown;
578        };
579
580        match connector_cell.get() {
581            Some(connector) => connector.connectivity(url),
582            None => Connectivity::Unknown,
583        }
584    }
585
586    /// Return iroh-specific peer details if `url` is handled by the iroh
587    /// connector.
588    pub async fn iroh_peer_info(
589        &self,
590        url: &SafeUrl,
591        path_timeout: Duration,
592    ) -> ServerResult<Option<IrohPeerInfo>> {
593        let url = match self.inner.connection_overrides.get(url) {
594            Some(replacement) => replacement,
595            None => url,
596        };
597
598        let Some((init_fn, connector_cell)) = self.inner.connectors_lazy.get(url.scheme()) else {
599            return Ok(None);
600        };
601
602        let init_fn = init_fn.clone();
603        connector_cell
604            .get_or_try_init(|| async move { init_fn().await })
605            .await
606            .map_err(|e| {
607                ServerError::Transport(anyhow!(
608                    "Connector failed to initialize: {}",
609                    e.fmt_compact_anyhow()
610                ))
611            })?
612            .iroh_peer_info(url, path_timeout)
613            .await
614    }
615
616    /// Subscribe to transport-level connectivity changes across all
617    /// connectors managed by this registry.
618    ///
619    /// The receiver ticks whenever a connector observes a path change on
620    /// an existing connection (for example an iroh connection upgrading
621    /// from relay to direct). The carried `u64` is an opaque counter —
622    /// consumers should treat each update as a "re-read connectivity"
623    /// signal.
624    pub fn connectivity_change_notifier(&self) -> watch::Receiver<u64> {
625        self.inner.path_change.subscribe()
626    }
627}
628pub type DynConnector = Arc<dyn Connector>;
629
630#[async_trait]
631pub trait Connector: Send + Sync + 'static + Debug {
632    async fn connect_guardian(
633        &self,
634        url: &SafeUrl,
635        api_secret: Option<&str>,
636    ) -> ServerResult<DynGuaridianConnection>;
637
638    async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection>;
639
640    /// Report how a connection to `url` is currently reaching its peer.
641    fn connectivity(&self, url: &SafeUrl) -> Connectivity;
642
643    /// Return iroh-specific peer details if this connector supports them.
644    async fn iroh_peer_info(
645        &self,
646        _url: &SafeUrl,
647        _path_timeout: Duration,
648    ) -> ServerResult<Option<IrohPeerInfo>> {
649        Ok(None)
650    }
651}
652
653/// How a connection is currently reaching its peer.
654///
655/// Transports without a relay concept (WS, HTTP) are always
656/// [`Connectivity::Direct`]. Tor-routed connections report
657/// [`Connectivity::Tor`]. Iroh connections may be [`Connectivity::Direct`]
658/// (peer-to-peer), [`Connectivity::Relay`] (routed through a relay
659/// server), or [`Connectivity::Mixed`] (both paths active); for Iroh this
660/// can change at runtime as hole-punching succeeds or falls back.
661#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
662pub enum Connectivity {
663    Direct,
664    Relay,
665    Mixed,
666    Tor,
667    Unknown,
668}
669
670/// Per-peer connection state reported by the federation API.
671///
672/// [`PeerStatus::Connected`] carries the current [`Connectivity`] of the
673/// active connection; for Iroh this reflects the path at the moment of the
674/// emission and may be stale until the next pool-level change (relay→direct
675/// upgrades on an existing connection are not yet streamed).
676#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
677pub enum PeerStatus {
678    Disconnected,
679    Connected(Connectivity),
680}
681
682/// Iroh-specific reachability details for a guardian endpoint.
683#[derive(Debug, Clone, PartialEq, Eq)]
684pub struct IrohPeerInfo {
685    pub node_id: String,
686    pub connectivity: Connectivity,
687    pub direct_addr: Option<SocketAddr>,
688    pub known_direct_addrs: Vec<SocketAddr>,
689    pub relay_url: Option<String>,
690}
691
692/// Generic connection trait shared between [`IGuardianConnection`] and
693/// [`IGatewayConnection`]
694#[apply(async_trait_maybe_send!)]
695pub trait IConnection: Debug + Send + Sync + 'static {
696    fn is_connected(&self) -> bool;
697
698    async fn await_disconnection(&self);
699}
700
701/// A connection from api client to a federation guardian (type erased)
702pub type DynGuaridianConnection = Arc<dyn IGuardianConnection>;
703
704/// A connection from api client to a federation guardian
705#[async_trait]
706pub trait IGuardianConnection: IConnection + Debug + Send + Sync + 'static {
707    async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value>;
708
709    fn into_dyn(self) -> DynGuaridianConnection
710    where
711        Self: Sized,
712    {
713        Arc::new(self)
714    }
715}
716
717/// A connection from api client to a gateway (type erased)
718pub type DynGatewayConnection = Arc<dyn IGatewayConnection>;
719
720/// A connection from a client to a gateway
721#[apply(async_trait_maybe_send!)]
722pub trait IGatewayConnection: IConnection + Debug + Send + Sync + 'static {
723    async fn request(
724        &self,
725        password: Option<String>,
726        method: Method,
727        route: &str,
728        payload: Option<Value>,
729    ) -> ServerResult<Value>;
730
731    fn into_dyn(self) -> DynGatewayConnection
732    where
733        Self: Sized,
734    {
735        Arc::new(self)
736    }
737}
738
739#[derive(Debug)]
740pub struct ConnectionPool<T: IConnection + ?Sized> {
741    /// Available connectors which we can make connections
742    connectors: ConnectorRegistry,
743
744    active_connections: watch::Sender<BTreeSet<SafeUrl>>,
745
746    /// Connection pool
747    ///
748    /// Every entry in this map will be created on demand and correspond to a
749    /// single outgoing connection to a certain URL that is in the process
750    /// of being established, or we already established.
751    #[allow(clippy::type_complexity)]
752    connections: Arc<tokio::sync::Mutex<HashMap<SafeUrl, Arc<ConnectionState<T>>>>>,
753}
754
755impl<T: IConnection + ?Sized> Clone for ConnectionPool<T> {
756    fn clone(&self) -> Self {
757        Self {
758            connectors: self.connectors.clone(),
759            connections: self.connections.clone(),
760            active_connections: self.active_connections.clone(),
761        }
762    }
763}
764
765impl<T: IConnection + ?Sized> ConnectionPool<T> {
766    pub fn new(connectors: ConnectorRegistry) -> Self {
767        Self {
768            connectors,
769            connections: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
770            active_connections: watch::channel(BTreeSet::new()).0,
771        }
772    }
773
774    async fn get_or_init_pool_entry(&self, url: &SafeUrl) -> Arc<ConnectionState<T>> {
775        let mut pool_locked = self.connections.lock().await;
776        pool_locked
777            .entry(url.to_owned())
778            .and_modify(|entry_arc| {
779                // Check if existing connection is disconnected and reset the whole entry.
780                //
781                // This resets the state (like connectivity backoff), which is what we want.
782                // Since the (`OnceCell`) was already initialized, it means connection was
783                // successfully before, and disconnected afterwards.
784                if let Some(existing_conn) = entry_arc.connection.get()
785                    && !existing_conn.is_connected()
786                {
787                    trace!(
788                        target: LOG_CLIENT_NET_API,
789                        %url,
790                        "Existing connection is disconnected, removing from pool"
791                    );
792                    self.active_connections.send_modify(|v| {
793                        v.remove(url);
794                    });
795                    *entry_arc = Arc::new(ConnectionState::new_reconnecting());
796                }
797            })
798            .or_insert_with(|| Arc::new(ConnectionState::new_initial()))
799            .clone()
800    }
801
802    pub async fn get_or_create_connection<F, Fut>(
803        &self,
804        url: &SafeUrl,
805        api_secret: Option<&str>,
806        create_connection: F,
807    ) -> ServerResult<Arc<T>>
808    where
809        F: Fn(SafeUrl, Option<String>, ConnectorRegistry) -> Fut + Clone + Send + Sync + 'static,
810        Fut: Future<Output = ServerResult<Arc<T>>> + Send + 'static,
811    {
812        let pool_entry_arc = self.get_or_init_pool_entry(url).await;
813
814        let leader_tx = loop {
815            let mut leader_rx = {
816                let mut chan_locked = pool_entry_arc
817                    .merge_connection_attempts_chan
818                    .lock()
819                    .expect("locking error");
820
821                if chan_locked.is_closed() {
822                    let (leader_tx, leader_rx) = broadcast::channel(1);
823                    *chan_locked = leader_rx;
824                    // whoever was trying to connect last time is gone
825                    // we're out of this lame loop for followers
826                    break leader_tx;
827                }
828
829                // lets piggyback on the existing leader
830                chan_locked.resubscribe()
831            };
832
833            if let Ok(res) = leader_rx.recv().await {
834                match res {
835                    Ok(o) => return Ok(o),
836                    Err(err) => {
837                        return Err(ServerError::Connection(anyhow::format_err!("{}", err)));
838                    }
839                }
840            }
841        };
842
843        let conn = pool_entry_arc
844            .connection
845            .get_or_try_init(|| async {
846                let retry_delay = pool_entry_arc.pre_reconnect_delay();
847                fedimint_core::runtime::sleep(retry_delay).await;
848
849                trace!(target: LOG_CLIENT_NET_API, %url, "Attempting to create a new connection");
850                let res = create_connection(
851                    url.clone(),
852                    api_secret.map(std::string::ToString::to_string),
853                    self.connectors.clone(),
854                )
855                .await;
856
857                // If any other task was also waiting to connect, send them the connection
858                // result.
859                //
860                // Note: we want to send both Ok or Err, so `res?` is used only afterwards.
861                let _ = leader_tx.send(
862                    res.as_ref()
863                        .map(|o| o.clone())
864                        .map_err(|err| err.to_string()),
865                );
866
867                let conn = res?;
868
869                self.active_connections.send_modify(|v| {
870                    v.insert(url.clone());
871                });
872
873                fedimint_core::runtime::spawn("connection disconnect watch", {
874                    let conn = conn.clone();
875                    let s = self.clone();
876                    let url = url.clone();
877                    async move {
878                        // wait for this connection to disconnect
879                        conn.await_disconnection().await;
880                        // And afterwards, update `active_connections`.
881                        //
882                        // This will update the `active_connections` just like calling
883                        // `get_or_create_connection` normally do, but we will
884                        // not attempt to do anything with the result (i.e. try to connect).
885                        s.get_or_init_pool_entry(&url).await;
886                    }
887                });
888
889                Ok(conn)
890            })
891            .await?;
892
893        trace!(target: LOG_CLIENT_NET_API, %url, "Connection ready");
894        Ok(conn.clone())
895    }
896    /// Get receiver for changes in the active connections
897    pub fn get_active_connection_receiver(&self) -> watch::Receiver<BTreeSet<SafeUrl>> {
898        self.active_connections.subscribe()
899    }
900
901    pub async fn wait_for_initialized_connections(&self) {
902        self.connectors.wait_for_initialized_connections().await
903    }
904
905    /// Report how a connection to `url` is currently reaching its peer.
906    pub fn connectivity(&self, url: &SafeUrl) -> Connectivity {
907        self.connectors.connectivity(url)
908    }
909
910    /// Subscribe to transport-level connectivity changes observed by
911    /// the connectors underlying this pool.
912    pub fn connectivity_change_notifier(&self) -> watch::Receiver<u64> {
913        self.connectors.connectivity_change_notifier()
914    }
915}
916
917/// Inner part of [`ConnectionState`] preserving state between attempts to
918/// initialize [`ConnectionState::connection`]
919#[derive(Debug)]
920struct ConnectionStateInner {
921    fresh: bool,
922    backoff: FibonacciBackoff,
923}
924
925#[derive(Debug)]
926pub struct ConnectionState<T: ?Sized> {
927    /// Connection we are trying to or already established
928    pub connection: tokio::sync::OnceCell<Arc<T>>,
929
930    /// When tasks attempt to connect at the same time,
931    /// this is the receiving end of the channel where
932    /// the "leader" sends a result.
933    merge_connection_attempts_chan:
934        std::sync::Mutex<broadcast::Receiver<std::result::Result<Arc<T>, String>>>,
935
936    /// State that technically is protected every time by
937    /// the serialization of `OnceCell::get_or_try_init`, but
938    /// for Rust purposes needs to be locked.
939    inner: std::sync::Mutex<ConnectionStateInner>,
940}
941
942impl<T: ?Sized> ConnectionState<T> {
943    /// Create a new connection state for a first time connection
944    pub fn new_initial() -> Self {
945        Self {
946            connection: OnceCell::new(),
947            inner: std::sync::Mutex::new(ConnectionStateInner {
948                fresh: true,
949                backoff: custom_backoff(
950                    // First time connections start quick
951                    Duration::from_millis(5),
952                    Duration::from_secs(30),
953                    None,
954                ),
955            }),
956            merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
957        }
958    }
959
960    /// Create a new connection state for a connection that already failed, and
961    /// is being reset
962    pub fn new_reconnecting() -> Self {
963        Self {
964            connection: OnceCell::new(),
965            inner: std::sync::Mutex::new(ConnectionStateInner {
966                // set the attempts to 1, indicating that
967                fresh: false,
968                backoff: custom_backoff(
969                    // Connections after a disconnect start with some minimum delay
970                    Duration::from_millis(500),
971                    Duration::from_secs(30),
972                    None,
973                ),
974            }),
975            merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
976        }
977    }
978
979    /// Record the fact that an attempt to connect is being made, and return
980    /// time the caller should wait.
981    pub fn pre_reconnect_delay(&self) -> Duration {
982        let mut backoff_locked = self.inner.lock().expect("Locking failed");
983        let fresh = backoff_locked.fresh;
984
985        backoff_locked.fresh = false;
986
987        if fresh {
988            Duration::default()
989        } else {
990            backoff_locked.backoff.next().expect("Keeps retrying")
991        }
992    }
993}