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