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