cli: Implement `rad sync status` command
This command shows the sync status of a repository by consulting the database via the node. To make it work, we call into the address store to update the `repo-sync-status` table when receiving ref announcements that are relevant to us.
This commit is contained in:
parent
ca557909c2
commit
d3b0483f42
|
|
@ -7,6 +7,18 @@ For instance let's create an issue and sync it with the network:
|
||||||
$ rad issue open --title "Test `rad sync`" --description "Check that the command works" -q --no-announce
|
$ rad issue open --title "Test `rad sync`" --description "Check that the command works" -q --no-announce
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If we check the sync status, we see that our peers are out of sync:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rad sync status
|
||||||
|
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ● NID Address Status At Timestamp │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ● z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z eve.radicle.xyz:8776 out-of-sync f209c9f [ ... ] │
|
||||||
|
│ ● z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob.radicle.xyz:8776 out-of-sync f209c9f [ ... ] │
|
||||||
|
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
```
|
||||||
|
|
||||||
Now let's run `rad sync`. This will announce the issue refs to the network and
|
Now let's run `rad sync`. This will announce the issue refs to the network and
|
||||||
wait for nodes to announce that they have fetched those refs.
|
wait for nodes to announce that they have fetched those refs.
|
||||||
|
|
||||||
|
|
@ -57,3 +69,15 @@ $ rad sync --fetch --replicas 1
|
||||||
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z..
|
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6Mkux1…nVhib7Z..
|
||||||
✓ Fetched repository from 1 seed(s)
|
✓ Fetched repository from 1 seed(s)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
We can check the sync status again to make sure everything's in sync:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rad sync status
|
||||||
|
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ ● NID Address Status At Timestamp │
|
||||||
|
├────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ● z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z eve.radicle.xyz:8776 synced 9f615f9 [ ... ] │
|
||||||
|
│ ● z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk bob.radicle.xyz:8776 synced 9f615f9 [ ... ] │
|
||||||
|
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ use std::time;
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
|
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
use radicle::node::{FetchResult, FetchResults, Handle as _, Node};
|
use radicle::node::{FetchResult, FetchResults, Handle as _, Node, SyncStatus};
|
||||||
use radicle::prelude::{Id, NodeId, Profile};
|
use radicle::prelude::{Id, NodeId, Profile};
|
||||||
use radicle::storage::{ReadRepository, ReadStorage};
|
use radicle::storage::{ReadRepository, ReadStorage};
|
||||||
|
use radicle_term::Element;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
|
use crate::terminal::{Table, TableOptions};
|
||||||
|
|
||||||
pub const HELP: Help = Help {
|
pub const HELP: Help = Help {
|
||||||
name: "sync",
|
name: "sync",
|
||||||
|
|
@ -20,6 +22,7 @@ Usage
|
||||||
|
|
||||||
rad sync [--fetch | --announce] [<rid>] [<option>...]
|
rad sync [--fetch | --announce] [<rid>] [<option>...]
|
||||||
rad sync --inventory [<option>...]
|
rad sync --inventory [<option>...]
|
||||||
|
rad sync status [<rid>] [<option>...]
|
||||||
|
|
||||||
By default, the current repository is synchronized both ways.
|
By default, the current repository is synchronized both ways.
|
||||||
If an <rid> is specified, that repository is synced instead.
|
If an <rid> is specified, that repository is synced instead.
|
||||||
|
|
@ -40,6 +43,10 @@ Usage
|
||||||
If `--inventory` is specified, the node's inventory is announced to
|
If `--inventory` is specified, the node's inventory is announced to
|
||||||
the network. This mode does not take an `<rid>`.
|
the network. This mode does not take an `<rid>`.
|
||||||
|
|
||||||
|
Commands
|
||||||
|
|
||||||
|
status Display the sync status of a repository
|
||||||
|
|
||||||
Options
|
Options
|
||||||
|
|
||||||
--fetch, -f Turn on fetching (default: true)
|
--fetch, -f Turn on fetching (default: true)
|
||||||
|
|
@ -53,6 +60,13 @@ Options
|
||||||
"#,
|
"#,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub enum Operation {
|
||||||
|
Synchronize(SyncMode),
|
||||||
|
#[default]
|
||||||
|
Status,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum SyncMode {
|
pub enum SyncMode {
|
||||||
Repo {
|
Repo {
|
||||||
|
|
@ -99,7 +113,7 @@ pub struct Options {
|
||||||
pub rid: Option<Id>,
|
pub rid: Option<Id>,
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
pub timeout: time::Duration,
|
pub timeout: time::Duration,
|
||||||
pub sync: SyncMode,
|
pub op: Operation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args for Options {
|
impl Args for Options {
|
||||||
|
|
@ -115,6 +129,7 @@ impl Args for Options {
|
||||||
let mut inventory = false;
|
let mut inventory = false;
|
||||||
let mut replicas = None;
|
let mut replicas = None;
|
||||||
let mut seeds = Vec::new();
|
let mut seeds = Vec::new();
|
||||||
|
let mut op: Option<Operation> = None;
|
||||||
|
|
||||||
while let Some(arg) = parser.next()? {
|
while let Some(arg) = parser.next()? {
|
||||||
match arg {
|
match arg {
|
||||||
|
|
@ -154,9 +169,14 @@ impl Args for Options {
|
||||||
Long("help") | Short('h') => {
|
Long("help") | Short('h') => {
|
||||||
return Err(Error::Help.into());
|
return Err(Error::Help.into());
|
||||||
}
|
}
|
||||||
Value(val) if rid.is_none() => {
|
Value(val) if rid.is_none() => match val.to_string_lossy().as_ref() {
|
||||||
|
"s" | "status" => {
|
||||||
|
op = Some(Operation::Status);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
rid = Some(term::args::rid(&val)?);
|
rid = Some(term::args::rid(&val)?);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
arg => {
|
arg => {
|
||||||
return Err(anyhow!(arg.unexpected()));
|
return Err(anyhow!(arg.unexpected()));
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +215,7 @@ impl Args for Options {
|
||||||
rid,
|
rid,
|
||||||
verbose,
|
verbose,
|
||||||
timeout,
|
timeout,
|
||||||
sync,
|
op: op.unwrap_or(Operation::Synchronize(sync)),
|
||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
))
|
))
|
||||||
|
|
@ -220,8 +240,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match options.sync {
|
match options.op {
|
||||||
SyncMode::Repo { mode, direction } => {
|
Operation::Status => {
|
||||||
|
sync_status(rid, &mut node)?;
|
||||||
|
}
|
||||||
|
Operation::Synchronize(SyncMode::Repo { mode, direction }) => {
|
||||||
if [SyncDirection::Fetch, SyncDirection::Both].contains(&direction) {
|
if [SyncDirection::Fetch, SyncDirection::Both].contains(&direction) {
|
||||||
if !profile.tracking()?.is_repo_tracked(&rid)? {
|
if !profile.tracking()?.is_repo_tracked(&rid)? {
|
||||||
anyhow::bail!("repository {rid} is not tracked");
|
anyhow::bail!("repository {rid} is not tracked");
|
||||||
|
|
@ -240,13 +263,69 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
announce_refs(rid, mode, options.timeout, node, &profile)?;
|
announce_refs(rid, mode, options.timeout, node, &profile)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyncMode::Inventory => {
|
Operation::Synchronize(SyncMode::Inventory) => {
|
||||||
announce_inventory(node)?;
|
announce_inventory(node)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_status(rid: Id, node: &mut Node) -> anyhow::Result<()> {
|
||||||
|
let mut table = Table::<6, term::Label>::new(TableOptions::bordered());
|
||||||
|
let seeds: Vec<_> = node.seeds(rid)?.into();
|
||||||
|
|
||||||
|
table.push([
|
||||||
|
term::format::dim(String::from("●")).into(),
|
||||||
|
term::format::bold(String::from("NID")).into(),
|
||||||
|
term::format::bold(String::from("Address")).into(),
|
||||||
|
term::format::bold(String::from("Status")).into(),
|
||||||
|
term::format::bold(String::from("At")).into(),
|
||||||
|
term::format::bold(String::from("Timestamp")).into(),
|
||||||
|
]);
|
||||||
|
table.divider();
|
||||||
|
|
||||||
|
for seed in seeds {
|
||||||
|
let (icon, status, refs, time) = match seed.sync {
|
||||||
|
Some(SyncStatus::Synced { at }) => (
|
||||||
|
term::format::positive("●"),
|
||||||
|
term::format::positive("synced"),
|
||||||
|
term::label(term::format::oid(at.oid)),
|
||||||
|
term::format::timestamp(at.timestamp).into(),
|
||||||
|
),
|
||||||
|
Some(SyncStatus::OutOfSync { remote, .. }) => (
|
||||||
|
term::format::negative("●"),
|
||||||
|
term::format::negative("out-of-sync"),
|
||||||
|
term::label(term::format::oid(remote.oid)),
|
||||||
|
term::format::timestamp(remote.timestamp).into(),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
term::format::yellow("●"),
|
||||||
|
term::format::yellow("?"),
|
||||||
|
term::label("?"),
|
||||||
|
term::label("?"),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let addr = seed
|
||||||
|
.addrs
|
||||||
|
.first()
|
||||||
|
.map(|a| a.addr.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
table.push([
|
||||||
|
icon.into(),
|
||||||
|
seed.nid.to_human().into(),
|
||||||
|
addr,
|
||||||
|
status.into(),
|
||||||
|
refs,
|
||||||
|
time,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
table.print();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn announce_refs(
|
fn announce_refs(
|
||||||
rid: Id,
|
rid: Id,
|
||||||
_mode: RepoSync,
|
_mode: RepoSync,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,16 @@ fn test<'a>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test config.
|
||||||
|
fn config(alias: &'static str) -> Config {
|
||||||
|
Config {
|
||||||
|
external_addresses: vec![
|
||||||
|
node::Address::from_str(&format!("{alias}.radicle.xyz:8776")).unwrap(),
|
||||||
|
],
|
||||||
|
..Config::test(Alias::new(alias))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn formula(root: &Path, test: impl AsRef<Path>) -> Result<TestFormula, Box<dyn std::error::Error>> {
|
fn formula(root: &Path, test: impl AsRef<Path>) -> Result<TestFormula, Box<dyn std::error::Error>> {
|
||||||
let mut formula = TestFormula::new(root.to_path_buf());
|
let mut formula = TestFormula::new(root.to_path_buf());
|
||||||
let base = Path::new(env!("CARGO_MANIFEST_DIR"));
|
let base = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|
@ -1018,9 +1028,9 @@ fn test_cob_deletion() {
|
||||||
fn rad_sync() {
|
fn rad_sync() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
let working = environment.tmp().join("working");
|
let working = environment.tmp().join("working");
|
||||||
let alice = environment.node(Config::test(Alias::new("alice")));
|
let alice = environment.node(config("alice"));
|
||||||
let bob = environment.node(Config::test(Alias::new("bob")));
|
let bob = environment.node(config("bob"));
|
||||||
let eve = environment.node(Config::test(Alias::new("eve")));
|
let eve = environment.node(config("eve"));
|
||||||
let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
|
||||||
|
|
||||||
fixtures::repository(working.join("acme"));
|
fixtures::repository(working.join("acme"));
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ use crate::crypto::{Signer, Verified};
|
||||||
use crate::identity::{Doc, Id};
|
use crate::identity::{Doc, Id};
|
||||||
use crate::node::routing;
|
use crate::node::routing;
|
||||||
use crate::node::routing::InsertResult;
|
use crate::node::routing::InsertResult;
|
||||||
use crate::node::{Address, Alias, Features, FetchResult, HostName, Seed, Seeds, SyncStatus};
|
use crate::node::{
|
||||||
|
Address, Alias, Features, FetchResult, HostName, Seed, Seeds, SyncStatus, SyncedAt,
|
||||||
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::runtime::Emitter;
|
use crate::runtime::Emitter;
|
||||||
use crate::service::message::{Announcement, AnnouncementMessage, Info, Ping};
|
use crate::service::message::{Announcement, AnnouncementMessage, Info, Ping};
|
||||||
|
|
@ -116,6 +118,8 @@ pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Git(#[from] radicle::git::raw::Error),
|
Git(#[from] radicle::git::raw::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
GitExt(#[from] radicle::git::ext::Error),
|
||||||
|
#[error(transparent)]
|
||||||
Storage(#[from] storage::Error),
|
Storage(#[from] storage::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Refs(#[from] storage::refs::Error),
|
Refs(#[from] storage::refs::Error),
|
||||||
|
|
@ -1137,6 +1141,27 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update sync status for this repo.
|
||||||
|
if let Some(refs) = message.refs.iter().find(|r| &r.remote == self.nid()) {
|
||||||
|
match self
|
||||||
|
.addresses
|
||||||
|
.synced(&message.rid, announcer, refs.at, message.timestamp)
|
||||||
|
{
|
||||||
|
Ok(updated) => {
|
||||||
|
if updated {
|
||||||
|
debug!(
|
||||||
|
target: "service",
|
||||||
|
"Updating sync status of {announcer} for {} to {}",
|
||||||
|
message.rid, refs.at
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(target: "service", "Error updating sync status for {}: {e}", message.rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Buffer/throttle fetches.
|
// TODO: Buffer/throttle fetches.
|
||||||
let repo_entry = self.tracking.repo_policy(&message.rid).expect(
|
let repo_entry = self.tracking.repo_policy(&message.rid).expect(
|
||||||
"Service::handle_announcement: error accessing repo tracking configuration",
|
"Service::handle_announcement: error accessing repo tracking configuration",
|
||||||
|
|
@ -1531,7 +1556,9 @@ where
|
||||||
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
|
let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
|
||||||
|
|
||||||
for remote_id in remotes.into_iter() {
|
for remote_id in remotes.into_iter() {
|
||||||
if refs.push(RefsAt::new(&repo, remote_id)?).is_err() {
|
let refs_at = RefsAt::new(&repo, remote_id)?;
|
||||||
|
|
||||||
|
if refs.push(refs_at).is_err() {
|
||||||
warn!(
|
warn!(
|
||||||
target: "service",
|
target: "service",
|
||||||
"refs announcement limit ({}) exceeded, peers will see only some of your repository references",
|
"refs announcement limit ({}) exceeded, peers will see only some of your repository references",
|
||||||
|
|
@ -1634,9 +1661,11 @@ where
|
||||||
let synced = if local.at == seed.synced_at.oid {
|
let synced = if local.at == seed.synced_at.oid {
|
||||||
SyncStatus::Synced { at: seed.synced_at }
|
SyncStatus::Synced { at: seed.synced_at }
|
||||||
} else {
|
} else {
|
||||||
|
let local = SyncedAt::new(local.at, &repo)?;
|
||||||
|
|
||||||
SyncStatus::OutOfSync {
|
SyncStatus::OutOfSync {
|
||||||
local: local.at,
|
local,
|
||||||
remote: seed.synced_at.oid,
|
remote: seed.synced_at,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
seeds.insert(Seed::new(seed.nid, seed.addresses, state, Some(synced)));
|
seeds.insert(Seed::new(seed.nid, seed.addresses, state, Some(synced)));
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,9 @@ pub enum SyncStatus {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
OutOfSync {
|
OutOfSync {
|
||||||
/// Local head of our `rad/sigrefs`.
|
/// Local head of our `rad/sigrefs`.
|
||||||
local: git_ext::Oid,
|
local: SyncedAt,
|
||||||
/// Remote head of our `rad/sigrefs`.
|
/// Remote head of our `rad/sigrefs`.
|
||||||
remote: git_ext::Oid,
|
remote: SyncedAt,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ use localtime::LocalTime;
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
|
||||||
use crate::collections::RandomMap;
|
use crate::collections::RandomMap;
|
||||||
|
use crate::git;
|
||||||
use crate::node::{Address, Alias};
|
use crate::node::{Address, Alias};
|
||||||
use crate::prelude::{NodeId, Timestamp};
|
use crate::prelude::{NodeId, Timestamp};
|
||||||
|
use crate::storage::ReadRepository;
|
||||||
use crate::{node, profile};
|
use crate::{node, profile};
|
||||||
|
|
||||||
/// A map with the ability to randomly select values.
|
/// A map with the ability to randomly select values.
|
||||||
|
|
@ -193,6 +195,16 @@ pub struct SyncedAt {
|
||||||
pub timestamp: LocalTime,
|
pub timestamp: LocalTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SyncedAt {
|
||||||
|
/// Create a new [`SyncedAt`] given an OID, by looking up the timestamp in the repo.
|
||||||
|
pub fn new<S: ReadRepository>(oid: git::ext::Oid, repo: &S) -> Result<Self, git::ext::Error> {
|
||||||
|
let timestamp = repo.commit(oid)?.time();
|
||||||
|
let timestamp = LocalTime::from_secs(timestamp.seconds() as u64);
|
||||||
|
|
||||||
|
Ok(Self { oid, timestamp })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Seed of a specific repository.
|
/// Seed of a specific repository.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue