pub mod error;
pub mod http;
pub mod iroh;
pub mod metrics;
#[cfg(all(feature = "tor", not(target_family = "wasm")))]
pub mod tor;
pub mod ws;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt::{self, Debug};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, bail};
use async_trait::async_trait;
use fedimint_core::envs::{FM_WS_API_CONNECT_OVERRIDES_ENV, parse_kv_list_from_env};
use fedimint_core::module::{ApiMethod, ApiRequestErased};
use fedimint_core::util::backoff_util::{FibonacciBackoff, custom_backoff};
use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl};
use fedimint_core::{apply, async_trait_maybe_send};
use fedimint_logging::{LOG_CLIENT_NET_API, LOG_NET};
use fedimint_metrics::HistogramExt as _;
use reqwest::Method;
use serde_json::Value;
use tokio::sync::{OnceCell, SetOnce, broadcast, watch};
use tracing::trace;
use crate::error::ServerError;
use crate::metrics::{CONNECTION_ATTEMPTS_TOTAL, CONNECTION_DURATION_SECONDS};
use crate::ws::WebsocketConnector;
pub type ServerResult<T> = Result<T, ServerError>;
type ConnectorInitFn = Arc<
dyn Fn() -> Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>> + Send + Sync,
>;
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct ConnectorRegistryBuilder {
connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
iroh_enable: bool,
iroh_dns: Option<SafeUrl>,
iroh_next: bool,
iroh_pkarr_dht: bool,
ws_enable: bool,
ws_force_tor: bool,
http_enable: bool,
}
impl ConnectorRegistryBuilder {
#[allow(clippy::unused_async)] pub async fn bind(self) -> anyhow::Result<ConnectorRegistry> {
let mut connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)> =
BTreeMap::new();
let builder_ws = self.clone();
let ws_connector_init = Arc::new(move || {
let builder = builder_ws.clone();
Box::pin(async move { builder.build_ws_connector().await })
as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
});
connectors_lazy.insert("ws".into(), (ws_connector_init.clone(), OnceCell::new()));
connectors_lazy.insert("wss".into(), (ws_connector_init.clone(), OnceCell::new()));
let builder_iroh = self.clone();
connectors_lazy.insert(
"iroh".into(),
(
Arc::new(move || {
let builder = builder_iroh.clone();
Box::pin(async move { builder.build_iroh_connector().await })
as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
}),
OnceCell::new(),
),
);
let builder_http = self.clone();
let http_connector_init = Arc::new(move || {
let builder = builder_http.clone();
Box::pin(async move { builder.build_http_connector() })
as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
});
connectors_lazy.insert(
"http".into(),
(http_connector_init.clone(), OnceCell::new()),
);
connectors_lazy.insert(
"https".into(),
(http_connector_init.clone(), OnceCell::new()),
);
Ok(ConnectorRegistry {
inner: ConnectorRegistryInner {
connectors_lazy,
connection_overrides: self.connection_overrides,
initialized: SetOnce::new(),
}
.into(),
})
}
pub async fn build_iroh_connector(&self) -> anyhow::Result<DynConnector> {
if !self.iroh_enable {
bail!("Iroh connector not enabled");
}
Ok(Arc::new(
iroh::IrohConnector::new(self.iroh_dns.clone(), self.iroh_pkarr_dht, self.iroh_next)
.await?,
) as DynConnector)
}
pub async fn build_ws_connector(&self) -> anyhow::Result<DynConnector> {
if !self.ws_enable {
bail!("Websocket connector not enabled");
}
match self.ws_force_tor {
#[cfg(all(feature = "tor", not(target_family = "wasm")))]
true => {
use crate::tor::TorConnector;
Ok(Arc::new(TorConnector::bootstrap().await?) as DynConnector)
}
false => Ok(Arc::new(WebsocketConnector::new()) as DynConnector),
#[allow(unreachable_patterns)]
_ => bail!("Tor requested, but not support not compiled in"),
}
}
pub fn build_http_connector(&self) -> anyhow::Result<DynConnector> {
if !self.http_enable {
bail!("Http connector not enabled");
}
Ok(Arc::new(crate::http::HttpConnector::default()) as DynConnector)
}
pub fn iroh_pkarr_dht(self, enable: bool) -> Self {
Self {
iroh_pkarr_dht: enable,
..self
}
}
pub fn iroh_next(self, enable: bool) -> Self {
Self {
iroh_next: enable,
..self
}
}
pub fn ws_force_tor(self, enable: bool) -> Self {
Self {
ws_force_tor: enable,
..self
}
}
pub fn http(self, enable: bool) -> Self {
Self {
http_enable: enable,
..self
}
}
pub fn set_iroh_dns(self, url: SafeUrl) -> Self {
Self {
iroh_dns: Some(url),
..self
}
}
pub fn with_env_var_overrides(mut self) -> anyhow::Result<Self> {
for (k, v) in parse_kv_list_from_env::<_, SafeUrl>(FM_WS_API_CONNECT_OVERRIDES_ENV)? {
self = self.with_connection_override(k, v);
}
Ok(Self { ..self })
}
pub fn with_connection_override(
mut self,
original_url: SafeUrl,
replacement_url: SafeUrl,
) -> Self {
self.connection_overrides
.insert(original_url, replacement_url);
self
}
}
struct ConnectorRegistryInner {
connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)>,
connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
initialized: tokio::sync::SetOnce<()>,
}
#[derive(Clone)]
pub struct ConnectorRegistry {
inner: Arc<ConnectorRegistryInner>,
}
impl fmt::Debug for ConnectorRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConnectorRegistry")
.field("connectors_lazy", &self.inner.connectors_lazy.len())
.field("connection_overrides", &self.inner.connection_overrides)
.finish()
}
}
impl ConnectorRegistry {
pub fn build_from_client_defaults() -> ConnectorRegistryBuilder {
ConnectorRegistryBuilder {
iroh_enable: true,
iroh_dns: None,
iroh_pkarr_dht: false,
iroh_next: true,
ws_enable: true,
ws_force_tor: false,
http_enable: true,
connection_overrides: BTreeMap::default(),
}
}
pub fn build_from_server_defaults() -> ConnectorRegistryBuilder {
ConnectorRegistryBuilder {
iroh_enable: true,
iroh_dns: None,
iroh_pkarr_dht: true,
iroh_next: true,
ws_enable: true,
ws_force_tor: false,
http_enable: false,
connection_overrides: BTreeMap::default(),
}
}
pub fn build_from_testing_defaults() -> ConnectorRegistryBuilder {
ConnectorRegistryBuilder {
iroh_enable: true,
iroh_dns: None,
iroh_pkarr_dht: false,
iroh_next: false,
ws_enable: true,
ws_force_tor: false,
http_enable: true,
connection_overrides: BTreeMap::default(),
}
}
pub fn build_from_client_env() -> anyhow::Result<ConnectorRegistryBuilder> {
let builder = Self::build_from_client_defaults().with_env_var_overrides()?;
Ok(builder)
}
pub fn build_from_server_env() -> anyhow::Result<ConnectorRegistryBuilder> {
let builder = Self::build_from_server_defaults().with_env_var_overrides()?;
Ok(builder)
}
pub fn build_from_testing_env() -> anyhow::Result<ConnectorRegistryBuilder> {
let builder = Self::build_from_testing_defaults().with_env_var_overrides()?;
Ok(builder)
}
pub async fn wait_for_initialized_connections(&self) {
self.inner.initialized.wait().await;
}
pub async fn connect_guardian(
&self,
url: &SafeUrl,
api_secret: Option<&str>,
) -> ServerResult<DynGuaridianConnection> {
trace!(
target: LOG_NET,
%url,
"Connection requested to guardian"
);
let _ = self.inner.initialized.set(());
let url = match self.inner.connection_overrides.get(url) {
Some(replacement) => {
trace!(
target: LOG_NET,
original_url = %url,
replacement_url = %replacement,
"Using a connectivity override for connection"
);
replacement
}
None => url,
};
let scheme = url.scheme().to_string();
let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
return Err(ServerError::InvalidEndpoint(anyhow!(
"Unsupported scheme: {}; missing endpoint handler",
url.scheme()
)));
};
let init_fn = connector_lazy.0.clone();
let timer = CONNECTION_DURATION_SECONDS
.with_label_values(&[&scheme])
.start_timer_ext();
let result = connector_lazy
.1
.get_or_try_init(|| async move { init_fn().await })
.await
.map_err(|e| {
ServerError::Transport(anyhow!(
"Connector failed to initialize: {}",
e.fmt_compact_anyhow()
))
})?
.connect_guardian(url, api_secret)
.await;
timer.observe_duration();
let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
CONNECTION_ATTEMPTS_TOTAL
.with_label_values(&[&scheme, &result_label])
.inc();
let conn = result.inspect_err(|err| {
trace!(
target: LOG_NET,
%url,
err = %err.fmt_compact(),
"Connection failed"
);
})?;
trace!(
target: LOG_NET,
%url,
"Connection returned"
);
Ok(conn)
}
pub async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection> {
trace!(
target: LOG_NET,
%url,
"Connection requested to gateway"
);
let _ = self.inner.initialized.set(());
let url = match self.inner.connection_overrides.get(url) {
Some(replacement) => {
trace!(
target: LOG_NET,
original_url = %url,
replacement_url = %replacement,
"Using a connectivity override for connection"
);
replacement
}
None => url,
};
let scheme = url.scheme().to_string();
let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
return Err(anyhow!(
"Unsupported scheme: {}; missing endpoint handler",
url.scheme()
));
};
let init_fn = connector_lazy.0.clone();
let timer = CONNECTION_DURATION_SECONDS
.with_label_values(&[&scheme])
.start_timer_ext();
let result = connector_lazy
.1
.get_or_try_init(|| async move { init_fn().await })
.await
.map_err(|e| {
ServerError::Transport(anyhow!(
"Connector failed to initialize: {}",
e.fmt_compact_anyhow()
))
})?
.connect_gateway(url)
.await;
timer.observe_duration();
let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
CONNECTION_ATTEMPTS_TOTAL
.with_label_values(&[&scheme, &result_label])
.inc();
result
}
}
pub type DynConnector = Arc<dyn Connector>;
#[async_trait]
pub trait Connector: Send + Sync + 'static + Debug {
async fn connect_guardian(
&self,
url: &SafeUrl,
api_secret: Option<&str>,
) -> ServerResult<DynGuaridianConnection>;
async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection>;
}
#[apply(async_trait_maybe_send!)]
pub trait IConnection: Debug + Send + Sync + 'static {
fn is_connected(&self) -> bool;
async fn await_disconnection(&self);
}
pub type DynGuaridianConnection = Arc<dyn IGuardianConnection>;
#[async_trait]
pub trait IGuardianConnection: IConnection + Debug + Send + Sync + 'static {
async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value>;
fn into_dyn(self) -> DynGuaridianConnection
where
Self: Sized,
{
Arc::new(self)
}
}
pub type DynGatewayConnection = Arc<dyn IGatewayConnection>;
#[apply(async_trait_maybe_send!)]
pub trait IGatewayConnection: IConnection + Debug + Send + Sync + 'static {
async fn request(
&self,
password: Option<String>,
method: Method,
route: &str,
payload: Option<Value>,
) -> ServerResult<Value>;
fn into_dyn(self) -> DynGatewayConnection
where
Self: Sized,
{
Arc::new(self)
}
}
#[derive(Debug)]
pub struct ConnectionPool<T: IConnection + ?Sized> {
connectors: ConnectorRegistry,
active_connections: watch::Sender<BTreeSet<SafeUrl>>,
#[allow(clippy::type_complexity)]
connections: Arc<tokio::sync::Mutex<HashMap<SafeUrl, Arc<ConnectionState<T>>>>>,
}
impl<T: IConnection + ?Sized> Clone for ConnectionPool<T> {
fn clone(&self) -> Self {
Self {
connectors: self.connectors.clone(),
connections: self.connections.clone(),
active_connections: self.active_connections.clone(),
}
}
}
impl<T: IConnection + ?Sized> ConnectionPool<T> {
pub fn new(connectors: ConnectorRegistry) -> Self {
Self {
connectors,
connections: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
active_connections: watch::channel(BTreeSet::new()).0,
}
}
async fn get_or_init_pool_entry(&self, url: &SafeUrl) -> Arc<ConnectionState<T>> {
let mut pool_locked = self.connections.lock().await;
pool_locked
.entry(url.to_owned())
.and_modify(|entry_arc| {
if let Some(existing_conn) = entry_arc.connection.get()
&& !existing_conn.is_connected()
{
trace!(
target: LOG_CLIENT_NET_API,
%url,
"Existing connection is disconnected, removing from pool"
);
self.active_connections.send_modify(|v| {
v.remove(url);
});
*entry_arc = Arc::new(ConnectionState::new_reconnecting());
}
})
.or_insert_with(|| Arc::new(ConnectionState::new_initial()))
.clone()
}
pub async fn get_or_create_connection<F, Fut>(
&self,
url: &SafeUrl,
api_secret: Option<&str>,
create_connection: F,
) -> ServerResult<Arc<T>>
where
F: Fn(SafeUrl, Option<String>, ConnectorRegistry) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = ServerResult<Arc<T>>> + Send + 'static,
{
let pool_entry_arc = self.get_or_init_pool_entry(url).await;
let leader_tx = loop {
let mut leader_rx = {
let mut chan_locked = pool_entry_arc
.merge_connection_attempts_chan
.lock()
.expect("locking error");
if chan_locked.is_closed() {
let (leader_tx, leader_rx) = broadcast::channel(1);
*chan_locked = leader_rx;
break leader_tx;
}
chan_locked.resubscribe()
};
if let Ok(res) = leader_rx.recv().await {
match res {
Ok(o) => return Ok(o),
Err(err) => {
return Err(ServerError::Connection(anyhow::format_err!("{}", err)));
}
}
}
};
let conn = pool_entry_arc
.connection
.get_or_try_init(|| async {
let retry_delay = pool_entry_arc.pre_reconnect_delay();
fedimint_core::runtime::sleep(retry_delay).await;
trace!(target: LOG_CLIENT_NET_API, %url, "Attempting to create a new connection");
let res = create_connection(
url.clone(),
api_secret.map(std::string::ToString::to_string),
self.connectors.clone(),
)
.await;
let _ = leader_tx.send(
res.as_ref()
.map(|o| o.clone())
.map_err(|err| err.to_string()),
);
let conn = res?;
self.active_connections.send_modify(|v| {
v.insert(url.clone());
});
fedimint_core::runtime::spawn("connection disconnect watch", {
let conn = conn.clone();
let s = self.clone();
let url = url.clone();
async move {
conn.await_disconnection().await;
s.get_or_init_pool_entry(&url).await;
}
});
Ok(conn)
})
.await?;
trace!(target: LOG_CLIENT_NET_API, %url, "Connection ready");
Ok(conn.clone())
}
pub fn get_active_connection_receiver(&self) -> watch::Receiver<BTreeSet<SafeUrl>> {
self.active_connections.subscribe()
}
pub async fn wait_for_initialized_connections(&self) {
self.connectors.wait_for_initialized_connections().await
}
}
#[derive(Debug)]
struct ConnectionStateInner {
fresh: bool,
backoff: FibonacciBackoff,
}
#[derive(Debug)]
pub struct ConnectionState<T: ?Sized> {
pub connection: tokio::sync::OnceCell<Arc<T>>,
merge_connection_attempts_chan:
std::sync::Mutex<broadcast::Receiver<std::result::Result<Arc<T>, String>>>,
inner: std::sync::Mutex<ConnectionStateInner>,
}
impl<T: ?Sized> ConnectionState<T> {
pub fn new_initial() -> Self {
Self {
connection: OnceCell::new(),
inner: std::sync::Mutex::new(ConnectionStateInner {
fresh: true,
backoff: custom_backoff(
Duration::from_millis(5),
Duration::from_secs(30),
None,
),
}),
merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
}
}
pub fn new_reconnecting() -> Self {
Self {
connection: OnceCell::new(),
inner: std::sync::Mutex::new(ConnectionStateInner {
fresh: false,
backoff: custom_backoff(
Duration::from_millis(500),
Duration::from_secs(30),
None,
),
}),
merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
}
}
pub fn pre_reconnect_delay(&self) -> Duration {
let mut backoff_locked = self.inner.lock().expect("Locking failed");
let fresh = backoff_locked.fresh;
backoff_locked.fresh = false;
if fresh {
Duration::default()
} else {
backoff_locked.backoff.next().expect("Keeps retrying")
}
}
}