diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 161 |
1 files changed, 47 insertions, 114 deletions
diff --git a/src/main.rs b/src/main.rs index f16138e..a10eb73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ -//! continuum: a replicated Automerge key-value store synchronized over Mycelium. +//! continuum: a replicated Automerge document, read and written by path, synchronized +//! over Mycelium, libp2p or Tor. //! //! main.rs only wires things together: arguments, the starting document, the node //! (`node`), bootstrap and the HTTP server. @@ -8,7 +9,6 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::{Context, Result}; -use automerge::AutoCommit; use tokio::sync::watch; use tracing::{debug, error, info, warn}; @@ -34,10 +34,10 @@ mod transport; mod units; use api::auth::AuthState; +use document::Document; use document::replica::Epoch; use swarm::bootstrap; use transport::Transport; -use transport::mycelium::{self, MyceliumSetup, MyceliumSpawnConfig}; #[tokio::main] async fn main() -> Result<()> { @@ -52,6 +52,19 @@ async fn main() -> Result<()> { return Ok(()); } let args = &config.settings; + if let Some(format) = logging::LogFormat::parse(&args.log_format) { + logging::set_format(format); + } + for (transport, built, options) in config.unused_transport_options() { + warn!( + transport, + built, + options = ?options, + "Options of a transport this node doesn't use are ignored" + ); + } + // Only with libp2p: without, `config` refuses `relay-only` already. + #[cfg(feature = "libp2p")] if args.relay_only { return relay_only(args).await; } @@ -63,11 +76,14 @@ async fn main() -> Result<()> { .context("node-key-file is required")?; let node_key = Arc::new(identity::NodeKey::load( node_key_file, - args.create_node_key, + args.node_key_create, )?); info!(node_id = %node_key.id(), "Node identity"); let swarm_key = match &args.swarm_key_file { - Some(path) => Some(Arc::new(swarm::auth::SwarmKey::from_file(path)?)), + Some(path) => Some(Arc::new(swarm::auth::SwarmKey::from_file( + path, + args.swarm_key_create, + )?)), None => { warn!("No swarm key: any node that can reach a member may join"); None @@ -75,34 +91,11 @@ async fn main() -> Result<()> { }; // First thing: until a handler is installed, SIGINT and SIGTERM kill the process on - // the spot, and startup (spawning Mycelium, bootstrap) can take minutes. + // the spot, and startup (starting transports, bootstrap) can take minutes. let stop = watch_for_shutdown().context("Installing signal handlers")?; // A broken htpasswd will abort startup. let auth_state = AuthState::new(args.htpasswd.as_deref().context("htpasswd is required")?)?; - let uses = |name: &str| args.transports.iter().any(|transport| transport == name); - let mut mycelium_setup = if uses("mycelium") { - let spawn_config = MyceliumSpawnConfig { - rest_url_opt: args.mycelium_rest_url(), - key_file_opt: args.mycelium_key_file.clone(), - network_key_file: args.mycelium_network_key_file.clone(), - network_name: args.mycelium_network_name.clone(), - tcp_port: args.mycelium_tcp_listen_port, - peers: args.mycelium_peers.clone(), - }; - let setup = tokio::select! { - setup = MyceliumSetup::bootstrap(spawn_config) => setup?, - _ = stopped(stop.clone()) => return Ok(()), - }; - // Without a password it may carry (the option is secret). - let mut shown_url = setup.rest_url.clone(); - let _ = shown_url.set_password(None); - debug!(url = shown_url.to_string(), "Using Mycelium REST API"); - Some(setup) - } else { - None - }; - info!( bind_addr = args.bind_addr.to_string(), transports = ?args.transports, @@ -158,7 +151,7 @@ async fn main() -> Result<()> { Some((starting.swarm_id, starting.origin)), starting.earlier_actors, ), - None => (AutoCommit::new(), None, None, Vec::new()), + None => (Document::new(), None, None, Vec::new()), }; let mut initial_doc = initial_doc; if let Some(spec) = &args.remake { @@ -176,46 +169,12 @@ async fn main() -> Result<()> { }; debug!(epoch = %initial_epoch, "Local epoch"); - // In the order of preference (`--transports`, checked when loading). + // The enabled, in the order of preference (`--transport-order`, checked when + // loading). let mut transports: Vec<(&'static str, Arc<dyn Transport>)> = Vec::new(); for name in &args.transports { - let transport: (&'static str, Arc<dyn Transport>) = match name.as_str() { - "mycelium" => { - let setup = mycelium_setup - .take() - .context("Mycelium named twice in transports")?; - ( - transport::mycelium::SCHEME, - Arc::new(mycelium::MyceliumTransport::connect(setup).await?), - ) - } - #[cfg(feature = "libp2p")] - "libp2p" => ( - transport::libp2p::SCHEME, - Arc::new(transport::libp2p::Libp2pTransport::start(libp2p_config( - args, false, - )?)?), - ), - #[cfg(feature = "tor")] - "tor" => { - let state_dir = args - .tor_state_dir - .clone() - .context("tor needs tor-state-dir")?; - let transport = tokio::select! { - transport = transport::tor::TorTransport::start(transport::tor::TorConfig { - key: Arc::clone(&node_key), - state_dir, - port: args.tor_port, - }) => transport?, - _ = stopped(stop.clone()) => return Ok(()), - }; - ( - transport::tor::SCHEME, - Arc::new(transport) as Arc<dyn Transport>, - ) - } - other => anyhow::bail!("transports: no {other} in this build"), + let Some(transport) = transport::start(name, args, &node_key, stop.clone()).await? else { + return Ok(()); }; info!( transport = transport.0, @@ -229,6 +188,12 @@ async fn main() -> Result<()> { .map(|(_, transport)| transport.delivery_limit()) .max() .unwrap_or_default(); + // An answer may come over any of them. + let sync_reply_timeout = transports + .iter() + .map(|(_, transport)| transport.sync_reply_timeout()) + .max() + .unwrap_or(state::SYNC_REPLY_TIMEOUT); let mut transports = transports.into_iter(); let (scheme, transport) = transports.next().context("No transports")?; @@ -250,10 +215,13 @@ async fn main() -> Result<()> { initial_epoch, initial_doc, earlier_actors, - timing: state::Timing::from_keepalive(args.keepalive, delivery_limit), + timing: state::Timing::new(args.keepalive, sync_reply_timeout, delivery_limit) + .with_probe_max(args.dormant_probe_max), neighbours_k: args.neighbours, alias: args.alias.clone(), electorate_window: args.electorate_window, + clock_skew_warn: args.clock_skew_warn, + superseded_writes: args.superseded_writes, // At most 4MiB (`Settings::check`). blob_chunk_bytes: args.blob_chunk as u32, swarm_key, @@ -298,15 +266,14 @@ async fn main() -> Result<()> { info!(address = %listener.local_addr()?, "HTTP API listening"); - let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); - + // The node shutting down (`AppState::closing`): the server drains. + let closing = state.closing.subscribe(); let server_builder = axum::serve(listener, app).with_graceful_shutdown(async move { - let _ = shutdown_rx.await; + stopped(closing).await; info!("Shutdown signal received. Draining API connections...") }); let server_fut = server_builder.into_future(); - let stopping = state.clone(); tokio::pin!(server_fut); // Joins, then runs until stopped; returns only if joining fails. @@ -328,14 +295,14 @@ async fn main() -> Result<()> { tokio::select! { failed = joining.as_mut() => { - let _ = shutdown_tx.send(()); + state.closing.send_replace(true); let _ = server_fut.as_mut().await; return failed; } exit_status = state.transport.wait_for_exit() => { error!(status = ?exit_status, "A transport stopped"); - let _ = shutdown_tx.send(()); + state.closing.send_replace(true); let _ = server_fut.as_mut().await; } @@ -348,7 +315,7 @@ async fn main() -> Result<()> { } _ = stopped(stop.clone()) => { - let _ = shutdown_tx.send(()); + state.closing.send_replace(true); let _ = server_fut.as_mut().await; } } @@ -358,41 +325,12 @@ async fn main() -> Result<()> { info!("Stopped while joining the swarm"); return Ok(()); } - swarm::peer::farewell(&stopping).await; - swarm::state_file::update(&stopping, true).await; - document::file::flush(&stopping).await; + swarm::peer::farewell(&state).await; + swarm::state_file::update(&state, true).await; + swarm::document_file::flush(&state).await; Ok(()) } -/// The libp2p transport's configuration; `relay_only`: a relay that is no node. -#[cfg(feature = "libp2p")] -fn libp2p_config( - args: &config::Settings, - relay_only: bool, -) -> Result<transport::libp2p::Libp2pConfig> { - let parse = |option: &str, addresses: &[String]| { - addresses - .iter() - .map(|address| { - address - .parse::<libp2p::Multiaddr>() - .with_context(|| format!("{option} {address:?}")) - }) - .collect::<Result<Vec<_>>>() - }; - Ok(transport::libp2p::Libp2pConfig { - key: transport::libp2p::Libp2pConfig::load_key(args.libp2p_key_file.as_deref())?, - listen: parse("libp2p-listen", &args.libp2p_listen)?, - announce: parse("libp2p-announce", &args.libp2p_announce)?, - relays: parse("libp2p-relays", &args.libp2p_relays)?, - relay: args.libp2p_relay || relay_only, - continuum: !relay_only, - mdns: args.libp2p_mdns, - public: args.libp2p_public, - memory: false, - }) -} - /// `--relay-only`: libp2p relaying for nodes of swarms, and the HTTP API's relay part /// (with `--htpasswd`), until stopped. #[cfg(feature = "libp2p")] @@ -400,7 +338,7 @@ async fn relay_only(args: &config::Settings) -> Result<()> { let stop = watch_for_shutdown().context("Installing signal handlers")?; let auth_state = args.htpasswd.as_deref().map(AuthState::new).transpose()?; let transport: Arc<dyn Transport> = Arc::new(transport::libp2p::Libp2pTransport::start( - libp2p_config(args, true)?, + transport::libp2p::Libp2pConfig::from_settings(args, true)?, )?); info!(peer_id = transport.local_id(), "Relaying only (no node)"); @@ -435,11 +373,6 @@ async fn relay_only(args: &config::Settings) -> Result<()> { Ok(()) } -#[cfg(not(feature = "libp2p"))] -async fn relay_only(_args: &config::Settings) -> Result<()> { - anyhow::bail!("relay-only relays over libp2p; this build has no libp2p") -} - /// Turns SIGINT and SIGTERM into a shutdown request. A second signal exits at once (the /// supervised Mycelium still gets its parent-death signal then). fn watch_for_shutdown() -> Result<watch::Receiver<bool>> { |
