cli: Unify all syncing code
We were using similar but slightly different syncing code in the remote-helper, issues/patches and `rad sync`. This commit unifies them.
This commit is contained in:
parent
8f89f088c6
commit
e6728db15a
|
|
@ -23,6 +23,7 @@ use radicle::storage::RepositoryError;
|
|||
|
||||
use crate::commands::rad_checkout as checkout;
|
||||
use crate::commands::rad_sync as sync;
|
||||
use crate::node::SyncSettings;
|
||||
use crate::project;
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
|
|
@ -62,7 +63,7 @@ pub struct Options {
|
|||
/// The seeding scope of the repository.
|
||||
scope: Scope,
|
||||
/// Sync settings.
|
||||
sync: sync::RepoSync,
|
||||
sync: SyncSettings,
|
||||
/// Fetch timeout.
|
||||
timeout: time::Duration,
|
||||
}
|
||||
|
|
@ -74,7 +75,7 @@ impl Args for Options {
|
|||
let mut parser = lexopt::Parser::from_args(args);
|
||||
let mut id: Option<RepoId> = None;
|
||||
let mut scope = Scope::All;
|
||||
let mut sync = sync::RepoSync::default();
|
||||
let mut sync = SyncSettings::default();
|
||||
let mut timeout = time::Duration::from_secs(9);
|
||||
let mut directory = None;
|
||||
|
||||
|
|
@ -150,8 +151,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
options.id,
|
||||
options.directory.clone(),
|
||||
options.scope,
|
||||
options.sync.with_profile(&profile),
|
||||
options.timeout,
|
||||
options.sync.with_profile(&profile).timeout(options.timeout),
|
||||
&mut node,
|
||||
&signer,
|
||||
&profile.storage,
|
||||
|
|
@ -236,8 +236,7 @@ pub fn clone<G: Signer>(
|
|||
id: RepoId,
|
||||
directory: Option<PathBuf>,
|
||||
scope: Scope,
|
||||
settings: sync::RepoSync,
|
||||
timeout: time::Duration,
|
||||
settings: SyncSettings,
|
||||
node: &mut Node,
|
||||
signer: &G,
|
||||
storage: &Storage,
|
||||
|
|
@ -260,7 +259,7 @@ pub fn clone<G: Signer>(
|
|||
);
|
||||
}
|
||||
|
||||
let results = sync::fetch(id, settings, timeout, node)?;
|
||||
let results = sync::fetch(id, settings, node)?;
|
||||
let Ok(repository) = storage.repository(id) else {
|
||||
// If we don't have the repository locally, even after attempting to fetch,
|
||||
// there's nothing we can do.
|
||||
|
|
|
|||
|
|
@ -593,7 +593,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
|
||||
if announce {
|
||||
let mut node = Node::new(profile.socket());
|
||||
node::announce(rid, &mut node)?;
|
||||
node::announce(
|
||||
&repo,
|
||||
node::SyncSettings::default(),
|
||||
node::SyncReporting::default(),
|
||||
&mut node,
|
||||
&profile,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -847,7 +847,13 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
|
||||
if announce {
|
||||
let mut node = Node::new(profile.socket());
|
||||
node::announce(rid, &mut node)?;
|
||||
node::announce(
|
||||
&repository,
|
||||
node::SyncSettings::default(),
|
||||
node::SyncReporting::default(),
|
||||
&mut node,
|
||||
&profile,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::str::FromStr;
|
||||
use std::time;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::git::RefString;
|
||||
|
|
@ -10,6 +9,7 @@ use radicle_crypto::PublicKey;
|
|||
use crate::commands::rad_checkout as checkout;
|
||||
use crate::commands::rad_follow as follow;
|
||||
use crate::commands::rad_sync as sync;
|
||||
use crate::node::SyncSettings;
|
||||
use crate::project::SetupRemote;
|
||||
|
||||
pub fn run(
|
||||
|
|
@ -31,8 +31,7 @@ pub fn run(
|
|||
follow::follow(*nid, alias, &mut node, profile)?;
|
||||
sync::fetch(
|
||||
rid,
|
||||
sync::RepoSync::default().with_profile(profile),
|
||||
time::Duration::from_secs(9),
|
||||
SyncSettings::default().with_profile(profile),
|
||||
&mut node,
|
||||
)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::ffi::OsString;
|
||||
use std::time;
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
||||
|
|
@ -10,6 +9,7 @@ use radicle::{prelude::*, storage, Node};
|
|||
use radicle_term::Element as _;
|
||||
|
||||
use crate::commands::rad_sync as sync;
|
||||
use crate::node::SyncSettings;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
use crate::{project, terminal as term};
|
||||
|
||||
|
|
@ -117,8 +117,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
if fetch && node.is_running() {
|
||||
sync::fetch(
|
||||
rid,
|
||||
sync::RepoSync::default().with_profile(&profile),
|
||||
time::Duration::from_secs(6),
|
||||
SyncSettings::default().with_profile(&profile),
|
||||
&mut node,
|
||||
)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsString;
|
||||
use std::ops::ControlFlow;
|
||||
use std::str::FromStr;
|
||||
use std::time;
|
||||
|
||||
|
|
@ -12,9 +11,11 @@ use radicle::node::AliasStore;
|
|||
use radicle::node::Seed;
|
||||
use radicle::node::{FetchResult, FetchResults, Handle as _, Node, SyncStatus};
|
||||
use radicle::prelude::{NodeId, Profile, RepoId};
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle_term::Element;
|
||||
|
||||
use crate::node::SyncReporting;
|
||||
use crate::node::SyncSettings;
|
||||
use crate::terminal as term;
|
||||
use crate::terminal::args::{Args, Error, Help};
|
||||
use crate::terminal::format::Author;
|
||||
|
|
@ -100,7 +101,7 @@ impl FromStr for SortBy {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SyncMode {
|
||||
Repo {
|
||||
settings: RepoSync,
|
||||
settings: SyncSettings,
|
||||
direction: SyncDirection,
|
||||
},
|
||||
Inventory,
|
||||
|
|
@ -109,58 +110,12 @@ pub enum SyncMode {
|
|||
impl Default for SyncMode {
|
||||
fn default() -> Self {
|
||||
Self::Repo {
|
||||
settings: RepoSync::default(),
|
||||
settings: SyncSettings::default(),
|
||||
direction: SyncDirection::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Repository sync settings.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RepoSync {
|
||||
/// Sync with at least N replicas.
|
||||
pub replicas: usize,
|
||||
/// Sync with the given list of seeds.
|
||||
pub seeds: BTreeSet<NodeId>,
|
||||
}
|
||||
|
||||
impl RepoSync {
|
||||
pub fn from_seeds(seeds: impl IntoIterator<Item = NodeId>) -> Self {
|
||||
let seeds = BTreeSet::from_iter(seeds);
|
||||
Self {
|
||||
replicas: seeds.len(),
|
||||
seeds,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use profile to populate sync settings, by adding preferred seeds if no seeds are specified,
|
||||
/// and removing the local node from the set.
|
||||
pub fn with_profile(mut self, profile: &Profile) -> Self {
|
||||
// If no seeds were specified, add up to `replica` seeds from the preferred seeds.
|
||||
if self.seeds.is_empty() {
|
||||
self.seeds = profile
|
||||
.config
|
||||
.preferred_seeds
|
||||
.iter()
|
||||
.map(|p| p.id)
|
||||
.take(self.replicas)
|
||||
.collect();
|
||||
}
|
||||
// Remove our local node from the seed set just in case it was added by mistake.
|
||||
self.seeds.remove(profile.id());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RepoSync {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
replicas: 3,
|
||||
seeds: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
||||
pub enum SyncDirection {
|
||||
Fetch,
|
||||
|
|
@ -174,7 +129,6 @@ pub struct Options {
|
|||
pub rid: Option<RepoId>,
|
||||
pub debug: bool,
|
||||
pub verbose: bool,
|
||||
pub timeout: time::Duration,
|
||||
pub sort_by: SortBy,
|
||||
pub op: Operation,
|
||||
}
|
||||
|
|
@ -266,16 +220,12 @@ impl Args for Options {
|
|||
(false, true) => SyncDirection::Announce,
|
||||
};
|
||||
let settings = if seeds.is_empty() {
|
||||
RepoSync {
|
||||
replicas: replicas.unwrap_or(3),
|
||||
seeds,
|
||||
}
|
||||
SyncSettings::from_replicas(replicas.unwrap_or(3))
|
||||
} else {
|
||||
RepoSync {
|
||||
replicas: replicas.unwrap_or(seeds.len()),
|
||||
seeds,
|
||||
}
|
||||
};
|
||||
SyncSettings::from_seeds(seeds)
|
||||
}
|
||||
.timeout(timeout);
|
||||
|
||||
SyncMode::Repo {
|
||||
settings,
|
||||
direction,
|
||||
|
|
@ -287,7 +237,6 @@ impl Args for Options {
|
|||
rid,
|
||||
debug,
|
||||
verbose,
|
||||
timeout,
|
||||
sort_by,
|
||||
op: op.unwrap_or(Operation::Synchronize(sync)),
|
||||
},
|
||||
|
|
@ -335,7 +284,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
if !profile.policies()?.is_seeding(&rid)? {
|
||||
anyhow::bail!("repository {rid} is not seeded");
|
||||
}
|
||||
let results = fetch(rid, settings.clone(), options.timeout, &mut node)?;
|
||||
let results = fetch(rid, settings.clone(), &mut node)?;
|
||||
let success = results.success().count();
|
||||
let failed = results.failed().count();
|
||||
|
||||
|
|
@ -348,14 +297,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
if [SyncDirection::Announce, SyncDirection::Both].contains(&direction) {
|
||||
announce_refs(
|
||||
rid,
|
||||
settings,
|
||||
options.timeout,
|
||||
options.debug,
|
||||
&mut node,
|
||||
&profile,
|
||||
)?;
|
||||
announce_refs(rid, settings, options.debug, &mut node, &profile)?;
|
||||
}
|
||||
}
|
||||
Operation::Synchronize(SyncMode::Inventory) => {
|
||||
|
|
@ -436,8 +378,7 @@ fn sync_status(
|
|||
|
||||
fn announce_refs(
|
||||
rid: RepoId,
|
||||
settings: RepoSync,
|
||||
timeout: time::Duration,
|
||||
settings: SyncSettings,
|
||||
debug: bool,
|
||||
node: &mut Node,
|
||||
profile: &Profile,
|
||||
|
|
@ -447,90 +388,18 @@ fn announce_refs(
|
|||
"nothing to announce, repository {rid} is not available locally"
|
||||
));
|
||||
};
|
||||
let doc = repo.identity_doc()?;
|
||||
let unsynced: Vec<_> = if doc.visibility.is_public() {
|
||||
// All seeds.
|
||||
let all = node.seeds(rid)?;
|
||||
// Seeds in sync with us.
|
||||
let synced = all.iter().filter(|s| s.is_synced());
|
||||
// Replicas not counting our local replica.
|
||||
let replicas = all
|
||||
.iter()
|
||||
.filter(|s| s.is_synced() && &s.nid != profile.id())
|
||||
.count();
|
||||
// Maximum replication factor we can achieve.
|
||||
let max_replicas = all.iter().filter(|s| &s.nid != profile.id()).count();
|
||||
// If the seeds we specified in the sync settings are all synced.
|
||||
let is_seeds_synced = {
|
||||
let synced = synced.map(|s| s.nid).collect::<BTreeSet<_>>();
|
||||
settings.seeds.iter().all(|s| synced.contains(s))
|
||||
};
|
||||
// If we met our desired replica count. Note that this can never exceed the maximum count.
|
||||
let is_replicas_synced = replicas >= settings.replicas.min(max_replicas);
|
||||
|
||||
// Nothing to do if we've met our sync state.
|
||||
if is_seeds_synced && is_replicas_synced {
|
||||
term::success!("Nothing to announce, already in sync with {replicas} node(s) (see `rad sync status`)");
|
||||
return Ok(());
|
||||
}
|
||||
// Return nodes we can announce to.
|
||||
all.connected()
|
||||
.filter(|s| !s.is_synced())
|
||||
.map(|s| s.nid)
|
||||
.collect()
|
||||
} else {
|
||||
node.sessions()?
|
||||
.into_iter()
|
||||
.filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid))
|
||||
.map(|s| s.nid)
|
||||
.collect()
|
||||
};
|
||||
crate::node::announce(
|
||||
&repo,
|
||||
settings,
|
||||
SyncReporting {
|
||||
debug,
|
||||
..SyncReporting::default()
|
||||
},
|
||||
node,
|
||||
profile,
|
||||
)?;
|
||||
|
||||
if unsynced.is_empty() {
|
||||
term::info!("Not connected to any seeds for {rid}.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut spinner = term::spinner(format!("Found {} seed(s)..", unsynced.len()));
|
||||
let result = node.announce(rid, unsynced, timeout, |event, replicas| match event {
|
||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
||||
node::AnnounceEvent::RefsSynced { remote, time } => {
|
||||
spinner.message(format!("Synced with {remote} in {time:?}.."));
|
||||
|
||||
// We're done syncing when both of these conditions are met:
|
||||
//
|
||||
// 1. We've matched or exceeded our target replica count.
|
||||
// 2. We've synced with the seeds specified manually.
|
||||
if replicas.len() >= settings.replicas
|
||||
&& settings.seeds.iter().all(|s| replicas.contains_key(s))
|
||||
{
|
||||
ControlFlow::Break(())
|
||||
} else {
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
if result.synced.is_empty() {
|
||||
spinner.failed();
|
||||
} else {
|
||||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
||||
spinner.finish();
|
||||
if debug {
|
||||
for (seed, time) in &result.synced {
|
||||
term::println(
|
||||
" ",
|
||||
term::format::dim(format!("Synced with {seed} in {time:?}")),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for seed in result.timed_out {
|
||||
term::notice!("Seed {seed} timed out..");
|
||||
}
|
||||
if result.synced.is_empty() {
|
||||
anyhow::bail!("all seeds timed out");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -546,8 +415,7 @@ pub fn announce_inventory(mut node: Node) -> anyhow::Result<()> {
|
|||
|
||||
pub fn fetch(
|
||||
rid: RepoId,
|
||||
settings: RepoSync,
|
||||
timeout: time::Duration,
|
||||
settings: SyncSettings,
|
||||
node: &mut Node,
|
||||
) -> Result<FetchResults, node::Error> {
|
||||
let local = node.nid()?;
|
||||
|
|
@ -567,7 +435,7 @@ pub fn fetch(
|
|||
term::warning(format!("node {nid} is not connected.. skipping"));
|
||||
continue;
|
||||
}
|
||||
let result = fetch_from(rid, nid, timeout, node)?;
|
||||
let result = fetch_from(rid, nid, settings.timeout, node)?;
|
||||
results.push(*nid, result);
|
||||
}
|
||||
if results.success().count() >= replicas {
|
||||
|
|
@ -582,7 +450,7 @@ pub fn fetch(
|
|||
.take(replicas)
|
||||
.collect::<Vec<_>>();
|
||||
for nid in connected {
|
||||
let result = fetch_from(rid, &nid, timeout, node)?;
|
||||
let result = fetch_from(rid, &nid, settings.timeout, node)?;
|
||||
results.push(nid, result);
|
||||
}
|
||||
|
||||
|
|
@ -598,10 +466,10 @@ pub fn fetch(
|
|||
if connect(
|
||||
seed.nid,
|
||||
seed.addrs.into_iter().map(|ka| ka.addr),
|
||||
timeout,
|
||||
settings.timeout,
|
||||
node,
|
||||
)? {
|
||||
let result = fetch_from(rid, &seed.nid, timeout, node)?;
|
||||
let result = fetch_from(rid, &seed.nid, settings.timeout, node)?;
|
||||
results.push(seed.nid, result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,267 @@
|
|||
use core::time;
|
||||
use std::collections::BTreeSet;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::ops::ControlFlow;
|
||||
use std::time::Duration;
|
||||
|
||||
use radicle::identity::RepoId;
|
||||
use radicle::node;
|
||||
use radicle::node::Handle as _;
|
||||
use radicle::Node;
|
||||
use radicle::node::{self, AnnounceResult};
|
||||
use radicle::node::{Handle as _, NodeId};
|
||||
use radicle::storage::{ReadRepository, RepositoryError};
|
||||
use radicle::{Node, Profile};
|
||||
use radicle_term::format;
|
||||
|
||||
use crate::terminal as term;
|
||||
|
||||
/// Announce changes to the network.
|
||||
pub fn announce(rid: RepoId, node: &mut Node) -> anyhow::Result<()> {
|
||||
match announce_(rid, node) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.is_connection_err() => {
|
||||
term::hint("Node is stopped. To announce changes to the network, start it with `rad node start`.");
|
||||
Ok(())
|
||||
/// Default time to wait for syncing to complete.
|
||||
pub const DEFAULT_SYNC_TIMEOUT: time::Duration = time::Duration::from_secs(9);
|
||||
|
||||
/// Repository sync settings.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SyncSettings {
|
||||
/// Sync with at least N replicas.
|
||||
pub replicas: usize,
|
||||
/// Sync with the given list of seeds.
|
||||
pub seeds: BTreeSet<NodeId>,
|
||||
/// How long to wait for syncing to complete.
|
||||
pub timeout: time::Duration,
|
||||
}
|
||||
|
||||
impl SyncSettings {
|
||||
/// Create a [`RepoSync`] from a list of seeds.
|
||||
pub fn from_seeds(seeds: impl IntoIterator<Item = NodeId>) -> Self {
|
||||
let seeds = BTreeSet::from_iter(seeds);
|
||||
Self {
|
||||
replicas: seeds.len(),
|
||||
seeds,
|
||||
timeout: DEFAULT_SYNC_TIMEOUT,
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
|
||||
/// Create a [`RepoSync`] from a replica count.
|
||||
pub fn from_replicas(replicas: usize) -> Self {
|
||||
Self {
|
||||
replicas,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set sync timeout. Defaults to [`DEFAULT_SYNC_TIMEOUT`].
|
||||
pub fn timeout(mut self, timeout: time::Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use profile to populate sync settings, by adding preferred seeds if no seeds are specified,
|
||||
/// and removing the local node from the set.
|
||||
pub fn with_profile(mut self, profile: &Profile) -> Self {
|
||||
// If no seeds were specified, add up to `replica` seeds from the preferred seeds.
|
||||
if self.seeds.is_empty() {
|
||||
self.seeds = profile
|
||||
.config
|
||||
.preferred_seeds
|
||||
.iter()
|
||||
.map(|p| p.id)
|
||||
.take(self.replicas)
|
||||
.collect();
|
||||
}
|
||||
// Remove our local node from the seed set just in case it was added by mistake.
|
||||
self.seeds.remove(profile.id());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn announce_(rid: RepoId, node: &mut Node) -> Result<(), radicle::node::Error> {
|
||||
let seeds = node.seeds(rid)?;
|
||||
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();
|
||||
impl Default for SyncSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
replicas: 3,
|
||||
seeds: BTreeSet::new(),
|
||||
timeout: DEFAULT_SYNC_TIMEOUT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if connected.is_empty() {
|
||||
term::info!("Not connected to any seeds.");
|
||||
return Ok(());
|
||||
/// Error while syncing.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SyncError {
|
||||
#[error(transparent)]
|
||||
Repository(#[from] RepositoryError),
|
||||
#[error(transparent)]
|
||||
Node(#[from] radicle::node::Error),
|
||||
#[error("all seeds timed out")]
|
||||
AllSeedsTimedOut,
|
||||
}
|
||||
|
||||
impl SyncError {
|
||||
fn is_connection_err(&self) -> bool {
|
||||
match self {
|
||||
Self::Node(e) => e.is_connection_err(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes sync output.
|
||||
#[derive(Debug)]
|
||||
pub enum SyncWriter {
|
||||
/// Write to standard out.
|
||||
Stdout(io::Stdout),
|
||||
/// Write to standard error.
|
||||
Stderr(io::Stderr),
|
||||
/// Discard output, like [`std::io::sink`].
|
||||
Sink,
|
||||
}
|
||||
|
||||
impl Clone for SyncWriter {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Stdout(_) => Self::Stdout(io::stdout()),
|
||||
Self::Stderr(_) => Self::Stderr(io::stderr()),
|
||||
Self::Sink => Self::Sink,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Write for SyncWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Stdout(stdout) => stdout.write(buf),
|
||||
Self::Stderr(stderr) => stderr.write(buf),
|
||||
Self::Sink => Ok(buf.len()),
|
||||
}
|
||||
}
|
||||
|
||||
let mut spinner = term::spinner(format!("Syncing with {} node(s)..", connected.len()));
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Stdout(stdout) => stdout.flush(),
|
||||
Self::Stderr(stderr) => stderr.flush(),
|
||||
Self::Sink => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures how sync progress is reported.
|
||||
pub struct SyncReporting {
|
||||
/// Progress messages or animations.
|
||||
pub progress: SyncWriter,
|
||||
/// Completion messages.
|
||||
pub completion: SyncWriter,
|
||||
/// Debug output.
|
||||
pub debug: bool,
|
||||
}
|
||||
|
||||
impl Default for SyncReporting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
progress: SyncWriter::Stderr(io::stderr()),
|
||||
completion: SyncWriter::Stdout(io::stdout()),
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Announce changes to the network.
|
||||
pub fn announce<R: ReadRepository>(
|
||||
repo: &R,
|
||||
settings: SyncSettings,
|
||||
reporting: SyncReporting,
|
||||
node: &mut Node,
|
||||
profile: &Profile,
|
||||
) -> Result<AnnounceResult, SyncError> {
|
||||
match announce_(repo, settings, reporting, node, profile) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) if e.is_connection_err() => {
|
||||
term::hint("Node is stopped. To announce changes to the network, start it with `rad node start`.");
|
||||
Ok(AnnounceResult::default())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn announce_<R: ReadRepository>(
|
||||
repo: &R,
|
||||
settings: SyncSettings,
|
||||
mut reporting: SyncReporting,
|
||||
node: &mut Node,
|
||||
profile: &Profile,
|
||||
) -> Result<AnnounceResult, SyncError> {
|
||||
let rid = repo.id();
|
||||
let doc = repo.identity_doc()?;
|
||||
let settings = settings.with_profile(profile);
|
||||
let unsynced: Vec<_> = if doc.visibility.is_public() {
|
||||
// All seeds.
|
||||
let all = node.seeds(rid)?;
|
||||
// Seeds in sync with us.
|
||||
let synced = all.iter().filter(|s| s.is_synced());
|
||||
// Replicas not counting our local replica.
|
||||
let replicas = all
|
||||
.iter()
|
||||
.filter(|s| s.is_synced() && &s.nid != profile.id())
|
||||
.count();
|
||||
// Maximum replication factor we can achieve.
|
||||
let max_replicas = all.iter().filter(|s| &s.nid != profile.id()).count();
|
||||
// If the seeds we specified in the sync settings are all synced.
|
||||
let is_seeds_synced = {
|
||||
let synced = synced.map(|s| s.nid).collect::<BTreeSet<_>>();
|
||||
settings.seeds.iter().all(|s| synced.contains(s))
|
||||
};
|
||||
// If we met our desired replica count. Note that this can never exceed the maximum count.
|
||||
let is_replicas_synced = replicas >= settings.replicas.min(max_replicas);
|
||||
|
||||
// Nothing to do if we've met our sync state.
|
||||
if is_seeds_synced && is_replicas_synced {
|
||||
term::success!(
|
||||
&mut reporting.completion;
|
||||
"Nothing to announce, already in sync with {replicas} node(s) (see `rad sync status`)"
|
||||
);
|
||||
return Ok(AnnounceResult::default());
|
||||
}
|
||||
// Return nodes we can announce to.
|
||||
all.connected()
|
||||
.filter(|s| !s.is_synced())
|
||||
.map(|s| s.nid)
|
||||
.collect()
|
||||
} else {
|
||||
node.sessions()?
|
||||
.into_iter()
|
||||
.filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid))
|
||||
.map(|s| s.nid)
|
||||
.collect()
|
||||
};
|
||||
|
||||
if unsynced.is_empty() {
|
||||
term::info!(&mut reporting.completion; "Not connected to any seeds for {rid}.");
|
||||
return Ok(AnnounceResult::default());
|
||||
}
|
||||
|
||||
let mut spinner = term::spinner_to(
|
||||
format!("Found {} seed(s)..", unsynced.len()),
|
||||
reporting.completion.clone(),
|
||||
reporting.progress.clone(),
|
||||
);
|
||||
let result = node.announce(
|
||||
rid,
|
||||
connected,
|
||||
Duration::from_secs(9),
|
||||
|event, _| match event {
|
||||
unsynced,
|
||||
settings.timeout,
|
||||
|event, replicas| match event {
|
||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
||||
node::AnnounceEvent::RefsSynced { remote, time } => {
|
||||
spinner.message(format!("Synced with {remote} in {time:?}.."));
|
||||
ControlFlow::Continue(())
|
||||
spinner.message(format!(
|
||||
"Synced with {} in {}..",
|
||||
format::dim(remote),
|
||||
format::dim(format!("{time:?}"))
|
||||
));
|
||||
|
||||
// We're done syncing when both of these conditions are met:
|
||||
//
|
||||
// 1. We've matched or exceeded our target replica count.
|
||||
// 2. We've synced with the seeds specified manually.
|
||||
if replicas.len() >= settings.replicas
|
||||
&& settings.seeds.iter().all(|s| replicas.contains_key(s))
|
||||
{
|
||||
ControlFlow::Break(())
|
||||
} else {
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
|
@ -48,6 +271,25 @@ fn announce_(rid: RepoId, node: &mut Node) -> Result<(), radicle::node::Error> {
|
|||
} else {
|
||||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
||||
spinner.finish();
|
||||
|
||||
if reporting.debug {
|
||||
for (seed, time) in &result.synced {
|
||||
writeln!(
|
||||
&mut reporting.completion,
|
||||
" {}",
|
||||
term::format::dim(format!("Synced with {seed} in {time:?}")),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
for seed in &result.timed_out {
|
||||
if settings.seeds.contains(seed) {
|
||||
term::notice!(&mut reporting.completion; "Seed {seed} timed out..");
|
||||
}
|
||||
}
|
||||
if result.synced.is_empty() {
|
||||
return Err(SyncError::AllSeedsTimedOut);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1054,7 +1054,7 @@ where
|
|||
self.fetch_refs_at(rid, from, refs, FETCH_TIMEOUT, channel);
|
||||
return;
|
||||
} else {
|
||||
debug!(target: "service", "Skipping dequeued fetch for {rid}, all refs are already in local storage");
|
||||
trace!(target: "service", "Skipping dequeued fetch for {rid}, all refs are already in local storage");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ pub struct Allow {
|
|||
pub struct Options {
|
||||
/// Don't sync after push.
|
||||
no_sync: bool,
|
||||
/// Sync debugging.
|
||||
sync_debug: bool,
|
||||
/// Enable hints.
|
||||
hints: bool,
|
||||
/// Open patch in draft mode.
|
||||
|
|
@ -210,6 +212,7 @@ fn push_option(args: &[&str], opts: &mut Options) -> Result<(), Error> {
|
|||
match args {
|
||||
["hints"] => opts.hints = true,
|
||||
["sync"] => opts.no_sync = false,
|
||||
["sync.debug"] => opts.sync_debug = true,
|
||||
["no-sync"] => opts.no_sync = true,
|
||||
["patch.draft"] => opts.draft = true,
|
||||
["allow.rollback"] => opts.allow.rollback = true,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::io::IsTerminal;
|
||||
use std::ops::ControlFlow;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time;
|
||||
use std::{assert_eq, io};
|
||||
|
||||
use thiserror::Error;
|
||||
|
|
@ -18,19 +16,16 @@ use radicle::explorer::ExplorerResource;
|
|||
use radicle::identity::Did;
|
||||
use radicle::node;
|
||||
use radicle::node::{Handle, NodeId};
|
||||
use radicle::prelude::RepoId;
|
||||
use radicle::storage;
|
||||
use radicle::storage::git::transport::local::Url;
|
||||
use radicle::storage::{ReadRepository, SignRepository as _, WriteRepository};
|
||||
use radicle::Profile;
|
||||
use radicle::{git, rad};
|
||||
use radicle_cli::terminal as cli;
|
||||
use radicle_cli as cli;
|
||||
use radicle_cli::terminal as term;
|
||||
|
||||
use crate::{hint, read_line, Options};
|
||||
|
||||
/// Default timeout for syncing to the network after a push.
|
||||
const DEFAULT_SYNC_TIMEOUT: time::Duration = time::Duration::from_secs(9);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Public key doesn't match the remote namespace we're pushing to.
|
||||
|
|
@ -89,7 +84,7 @@ pub enum Error {
|
|||
PatchCache(#[from] patch::cache::Error),
|
||||
/// Patch edit message error.
|
||||
#[error(transparent)]
|
||||
PatchEdit(#[from] cli::patch::Error),
|
||||
PatchEdit(#[from] term::patch::Error),
|
||||
/// Policy config error.
|
||||
#[error("node policy: {0}")]
|
||||
Policy(#[from] node::policy::config::Error),
|
||||
|
|
@ -329,8 +324,8 @@ pub fn run(
|
|||
if head.is_updated() {
|
||||
eprintln!(
|
||||
"{} Canonical head updated to {}",
|
||||
cli::format::positive("✓"),
|
||||
cli::format::secondary(head.new),
|
||||
term::format::positive("✓"),
|
||||
term::format::secondary(head.new),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +337,7 @@ pub fn run(
|
|||
let node = radicle::Node::new(profile.socket());
|
||||
if node.is_running() {
|
||||
// Nb. allow this to fail. The push to local storage was still successful.
|
||||
sync(stored.id, ok.into_values().flatten(), node, profile).ok();
|
||||
sync(stored, ok.into_values().flatten(), opts, node, profile).ok();
|
||||
} else if hints {
|
||||
hint("offline push, your node is not running");
|
||||
hint("to sync with the network, run `rad node start`");
|
||||
|
|
@ -397,7 +392,7 @@ fn patch_open<G: Signer>(
|
|||
return Err(Error::EmptyPatch);
|
||||
}
|
||||
let (title, description) =
|
||||
cli::patch::get_create_message(opts.message, &stored.backend, &base, &head)?;
|
||||
term::patch::get_create_message(opts.message, &stored.backend, &base, &head)?;
|
||||
|
||||
let patch = if opts.draft {
|
||||
patches.draft(
|
||||
|
|
@ -431,8 +426,8 @@ fn patch_open<G: Signer>(
|
|||
|
||||
eprintln!(
|
||||
"{} Patch {} {action}",
|
||||
cli::format::positive("✓"),
|
||||
cli::format::tertiary(patch),
|
||||
term::format::positive("✓"),
|
||||
term::format::tertiary(patch),
|
||||
);
|
||||
|
||||
// Create long-lived patch head reference, now that we know the Patch ID.
|
||||
|
|
@ -509,7 +504,7 @@ fn patch_update<G: Signer>(
|
|||
if patch.revisions().any(|(_, r)| *r.head() == commit.id()) {
|
||||
return Ok(None);
|
||||
}
|
||||
let message = cli::patch::get_update_message(
|
||||
let message = term::patch::get_update_message(
|
||||
opts.message,
|
||||
&stored.backend,
|
||||
patch.latest().1,
|
||||
|
|
@ -527,9 +522,9 @@ fn patch_update<G: Signer>(
|
|||
|
||||
eprintln!(
|
||||
"{} Patch {} updated to revision {}",
|
||||
cli::format::positive("✓"),
|
||||
cli::format::tertiary(cli::format::cob(&patch_id)),
|
||||
cli::format::dim(revision)
|
||||
term::format::positive("✓"),
|
||||
term::format::tertiary(term::format::cob(&patch_id)),
|
||||
term::format::dim(revision)
|
||||
);
|
||||
|
||||
// In this case, the patch was already merged via git, and pushed to storage.
|
||||
|
|
@ -641,15 +636,15 @@ fn patch_merge<C: cob::cache::Update<patch::Patch>, G: Signer>(
|
|||
if revision == latest {
|
||||
eprintln!(
|
||||
"{} Patch {} merged",
|
||||
cli::format::positive("✓"),
|
||||
cli::format::tertiary(merged.patch)
|
||||
term::format::positive("✓"),
|
||||
term::format::tertiary(merged.patch)
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} Patch {} merged at revision {}",
|
||||
cli::format::positive("✓"),
|
||||
cli::format::tertiary(merged.patch),
|
||||
cli::format::dim(cli::format::oid(revision)),
|
||||
term::format::positive("✓"),
|
||||
term::format::tertiary(merged.patch),
|
||||
term::format::dim(term::format::oid(revision)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -681,57 +676,38 @@ fn push_ref(
|
|||
|
||||
/// Sync with the network.
|
||||
fn sync(
|
||||
rid: RepoId,
|
||||
repo: &storage::git::Repository,
|
||||
updated: impl Iterator<Item = ExplorerResource>,
|
||||
opts: Options,
|
||||
mut node: radicle::Node,
|
||||
profile: &Profile,
|
||||
) -> Result<(), radicle::node::Error> {
|
||||
let seeds = node.seeds(rid)?;
|
||||
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();
|
||||
let mut replicated = HashSet::new();
|
||||
|
||||
if connected.is_empty() {
|
||||
eprintln!("Not connected to any seeds.");
|
||||
return Ok(());
|
||||
}
|
||||
let message = format!("Syncing with {} node(s)..", connected.len());
|
||||
let mut spinner = if io::stderr().is_terminal() {
|
||||
cli::spinner_to(message, io::stderr(), io::stderr())
|
||||
) -> Result<(), cli::node::SyncError> {
|
||||
let progress = if io::stderr().is_terminal() {
|
||||
cli::node::SyncWriter::Stderr(io::stderr())
|
||||
} else {
|
||||
cli::spinner_to(message, io::stderr(), io::sink())
|
||||
cli::node::SyncWriter::Sink
|
||||
};
|
||||
let result = node.announce(
|
||||
rid,
|
||||
connected,
|
||||
DEFAULT_SYNC_TIMEOUT,
|
||||
|event, _| match event {
|
||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
||||
node::AnnounceEvent::RefsSynced { remote, time } => {
|
||||
replicated.insert(remote);
|
||||
spinner.message(format!(
|
||||
"Synced with {} in {time:?}..",
|
||||
cli::format::dim(remote)
|
||||
));
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
let result = cli::node::announce(
|
||||
repo,
|
||||
cli::node::SyncSettings::default().with_profile(profile),
|
||||
cli::node::SyncReporting {
|
||||
progress,
|
||||
completion: cli::node::SyncWriter::Stderr(io::stderr()),
|
||||
debug: opts.sync_debug,
|
||||
},
|
||||
&mut node,
|
||||
profile,
|
||||
)?;
|
||||
|
||||
if result.synced.is_empty() {
|
||||
spinner.failed();
|
||||
} else {
|
||||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
||||
spinner.finish();
|
||||
}
|
||||
let mut urls = Vec::new();
|
||||
|
||||
for seed in profile.config.preferred_seeds.iter() {
|
||||
if replicated.contains(&seed.id) {
|
||||
if result.synced(&seed.id).is_some() {
|
||||
for resource in updated {
|
||||
let url = profile
|
||||
.config
|
||||
.public_explorer
|
||||
.url(seed.addr.host.clone(), rid)
|
||||
.url(seed.addr.host.clone(), repo.id)
|
||||
.resource(resource);
|
||||
|
||||
urls.push(url);
|
||||
|
|
@ -744,7 +720,7 @@ fn sync(
|
|||
if !urls.is_empty() {
|
||||
eprintln!();
|
||||
for url in urls {
|
||||
eprintln!(" {}", cli::format::dim(url));
|
||||
eprintln!(" {}", term::format::dim(url));
|
||||
}
|
||||
eprintln!();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
|
|||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($writer:expr; $($arg:tt)*) => ({
|
||||
writeln!($writer, $($arg)*).ok();
|
||||
});
|
||||
($($arg:tt)*) => ({
|
||||
println!("{}", format_args!($($arg)*));
|
||||
})
|
||||
|
|
@ -50,9 +53,14 @@ macro_rules! info {
|
|||
|
||||
#[macro_export]
|
||||
macro_rules! success {
|
||||
// Pattern when a writer is provided.
|
||||
($writer:expr; $($arg:tt)*) => ({
|
||||
$crate::io::success_args($writer, format_args!($($arg)*));
|
||||
});
|
||||
// Pattern without writer.
|
||||
($($arg:tt)*) => ({
|
||||
$crate::io::success_args(format_args!($($arg)*));
|
||||
})
|
||||
$crate::io::success_args(&mut std::io::stdout(), format_args!($($arg)*));
|
||||
});
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
|
@ -64,8 +72,12 @@ macro_rules! tip {
|
|||
|
||||
#[macro_export]
|
||||
macro_rules! notice {
|
||||
// Pattern when a writer is provided.
|
||||
($writer:expr; $($arg:tt)*) => ({
|
||||
$crate::io::notice_args($writer, format_args!($($arg)*));
|
||||
});
|
||||
($($arg:tt)*) => ({
|
||||
$crate::io::notice_args(format_args!($($arg)*));
|
||||
$crate::io::notice_args(&mut std::io::stdout(), format_args!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -74,8 +86,8 @@ pub use notice;
|
|||
pub use success;
|
||||
pub use tip;
|
||||
|
||||
pub fn success_args(args: fmt::Arguments) {
|
||||
println!("{} {args}", Paint::green("✓"));
|
||||
pub fn success_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
|
||||
writeln!(w, "{} {args}", Paint::green("✓")).ok();
|
||||
}
|
||||
|
||||
pub fn tip_args(args: fmt::Arguments) {
|
||||
|
|
@ -86,8 +98,8 @@ pub fn tip_args(args: fmt::Arguments) {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn notice_args(args: fmt::Arguments) {
|
||||
println!("{} {args}", Paint::new("!").dim());
|
||||
pub fn notice_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
|
||||
writeln!(w, "{} {args}", Paint::new("!").dim()).ok();
|
||||
}
|
||||
|
||||
pub fn columns() -> Option<usize> {
|
||||
|
|
|
|||
|
|
@ -637,7 +637,7 @@ impl From<Vec<Seed>> for Seeds {
|
|||
}
|
||||
|
||||
/// Announcement result returned by [`Node::announce`].
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AnnounceResult {
|
||||
/// Nodes that timed out.
|
||||
pub timed_out: Vec<NodeId>,
|
||||
|
|
@ -645,6 +645,16 @@ pub struct AnnounceResult {
|
|||
pub synced: Vec<(NodeId, time::Duration)>,
|
||||
}
|
||||
|
||||
impl AnnounceResult {
|
||||
/// Check if a node synced successfully.
|
||||
pub fn synced(&self, nid: &NodeId) -> Option<time::Duration> {
|
||||
self.synced
|
||||
.iter()
|
||||
.find(|(id, _)| id == nid)
|
||||
.map(|(_, time)| *time)
|
||||
}
|
||||
}
|
||||
|
||||
/// A sync event, emitted by [`Node::announce`].
|
||||
#[derive(Debug)]
|
||||
pub enum AnnounceEvent {
|
||||
|
|
|
|||
Loading…
Reference in New Issue