node: Only fetch from tracked nodes
The tracked nodes include nodes marked tracked in the db, and the delegates and `Namespaces::All` if it is a new repo. Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
parent
a5746b4f08
commit
b030e9ea80
|
|
@ -50,6 +50,7 @@ pub use crate::service::session::Session;
|
||||||
use self::gossip::Gossip;
|
use self::gossip::Gossip;
|
||||||
use self::message::InventoryAnnouncement;
|
use self::message::InventoryAnnouncement;
|
||||||
use self::reactor::Reactor;
|
use self::reactor::Reactor;
|
||||||
|
use self::tracking::NamespacesError;
|
||||||
|
|
||||||
/// Target number of peers to maintain connections to.
|
/// Target number of peers to maintain connections to.
|
||||||
pub const TARGET_OUTBOUND_PEERS: usize = 8;
|
pub const TARGET_OUTBOUND_PEERS: usize = 8;
|
||||||
|
|
@ -102,6 +103,8 @@ pub enum Error {
|
||||||
Routing(#[from] routing::Error),
|
Routing(#[from] routing::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Tracking(#[from] tracking::Error),
|
Tracking(#[from] tracking::Error),
|
||||||
|
#[error("namespaces error: {0}")]
|
||||||
|
Namespaces(#[from] NamespacesError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Function used to query internal service state.
|
/// Function used to query internal service state.
|
||||||
|
|
@ -871,27 +874,29 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Buffer/throttle fetches.
|
// TODO: Buffer/throttle fetches.
|
||||||
if self
|
let repo_entry = self.tracking.repo_policy(&message.rid).expect(
|
||||||
.tracking
|
"Service::handle_announcement: error accessing repo tracking configuration",
|
||||||
.is_repo_tracked(&message.rid)
|
);
|
||||||
.expect("Service::handle_announcement: error accessing tracking configuration")
|
|
||||||
{
|
if repo_entry.policy == tracking::Policy::Track {
|
||||||
// Refs can be relayed by peers who don't have the data in storage,
|
// Refs can be relayed by peers who don't have the data in storage,
|
||||||
// therefore we only check whether we are connected to the *announcer*,
|
// therefore we only check whether we are connected to the *announcer*,
|
||||||
// which is required by the protocol to only announce refs it has.
|
// which is required by the protocol to only announce refs it has.
|
||||||
if self.sessions.is_connected(announcer) {
|
if self.sessions.is_connected(announcer) {
|
||||||
match message.is_fresh(&self.storage) {
|
match self.should_fetch_refs_announcement(message, &repo_entry.scope) {
|
||||||
Ok(is_fresh) => {
|
Ok(true) => self.fetch(message.rid, announcer),
|
||||||
if is_fresh {
|
Ok(false) => {
|
||||||
// TODO: Only fetch if the refs announced are for peers we're tracking.
|
debug!(target: "service", "Skip fetch the refs from {announcer}")
|
||||||
self.fetch(message.rid, announcer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(target: "service", "Failed to check ref announcement freshness: {e}");
|
error!(target: "service", "Failed to check refs announcement: {e}");
|
||||||
|
return Err(session::Error::Misbehavior);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debug!(target: "service", "No sessions connected to {announcer}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(relay);
|
return Ok(relay);
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
|
|
@ -963,6 +968,53 @@ where
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A convenient method to check if we should fetch from a `RefsAnnouncement`
|
||||||
|
/// with `scope`.
|
||||||
|
fn should_fetch_refs_announcement(
|
||||||
|
&self,
|
||||||
|
message: &RefsAnnouncement,
|
||||||
|
scope: &tracking::Scope,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
// First, check the freshness.
|
||||||
|
if !message.is_fresh(&self.storage)? {
|
||||||
|
debug!(target: "service", "All refs of {} are already in the local node", &message.rid);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second, check the scope.
|
||||||
|
match scope {
|
||||||
|
tracking::Scope::All => Ok(true),
|
||||||
|
tracking::Scope::Trusted => {
|
||||||
|
match self.tracking.namespaces_for(&self.storage, &message.rid) {
|
||||||
|
Ok(Namespaces::All) => Ok(true),
|
||||||
|
Ok(Namespaces::Many(nodes)) => {
|
||||||
|
// Get the set of trusted nodes except self.
|
||||||
|
let my_id = self.node_id();
|
||||||
|
let node_set: HashSet<_> =
|
||||||
|
nodes.iter().filter(|key| *key != &my_id).collect();
|
||||||
|
|
||||||
|
// Check if there is at least one trusted ref.
|
||||||
|
Ok(message
|
||||||
|
.refs
|
||||||
|
.iter()
|
||||||
|
.any(|(pub_key, _refs)| node_set.contains(pub_key)))
|
||||||
|
}
|
||||||
|
Ok(Namespaces::One(key)) => {
|
||||||
|
Ok(message.refs.iter().any(|(pub_key, _refs)| pub_key == &key))
|
||||||
|
}
|
||||||
|
Err(NamespacesError::NoTrusted { rid }) => {
|
||||||
|
debug!(target: "service", "No trusted nodes to fetch {}", &rid);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(target: "service", "Failed to obtain namespaces: {e}");
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_message(
|
pub fn handle_message(
|
||||||
&mut self,
|
&mut self,
|
||||||
remote: &NodeId,
|
remote: &NodeId,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use crossbeam_channel as chan;
|
use crossbeam_channel as chan;
|
||||||
use log::*;
|
use log::*;
|
||||||
|
use radicle::storage::ReadRepository;
|
||||||
|
|
||||||
use crate::address;
|
use crate::address;
|
||||||
use crate::address::Store;
|
use crate::address::Store;
|
||||||
|
|
@ -250,7 +251,20 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refs_announcement(&self, rid: Id) -> Message {
|
pub fn refs_announcement(&self, rid: Id) -> Message {
|
||||||
let refs = BoundedVec::new();
|
let mut refs = BoundedVec::new();
|
||||||
|
if let Ok(repo) = self.storage().repository(rid) {
|
||||||
|
if let Ok(false) = repo.is_empty() {
|
||||||
|
if let Ok(remotes) = repo.remotes() {
|
||||||
|
for (remote_id, remote) in remotes.into_iter() {
|
||||||
|
if let Err(e) = refs.push((remote_id, remote.refs.unverified())) {
|
||||||
|
debug!(target: "test", "Failed to push {remote_id} to refs: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ann = AnnouncementMessage::from(RefsAnnouncement {
|
let ann = AnnouncementMessage::from(RefsAnnouncement {
|
||||||
rid,
|
rid,
|
||||||
refs,
|
refs,
|
||||||
|
|
|
||||||
|
|
@ -669,6 +669,98 @@ fn test_refs_announcement_relay() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Even if Alice is not tracking Bob, Alice will fetch Bob's refs for a repo she doesn't have.
|
||||||
|
#[test]
|
||||||
|
fn test_refs_announcement_fetch_trusted_no_inventory() {
|
||||||
|
logger::init(log::Level::Debug);
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let mut alice = Peer::config(
|
||||||
|
"alice",
|
||||||
|
[7, 7, 7, 7],
|
||||||
|
Storage::open(tmp.path().join("alice")).unwrap(),
|
||||||
|
peer::Config::default(),
|
||||||
|
);
|
||||||
|
let bob = {
|
||||||
|
let mut rng = fastrand::Rng::new();
|
||||||
|
let signer = MockSigner::new(&mut rng);
|
||||||
|
let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();
|
||||||
|
|
||||||
|
Peer::config(
|
||||||
|
"bob",
|
||||||
|
[9, 9, 9, 9],
|
||||||
|
storage,
|
||||||
|
peer::Config {
|
||||||
|
signer,
|
||||||
|
rng,
|
||||||
|
..peer::Config::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let bob_inv = bob.storage().inventory().unwrap();
|
||||||
|
let rid = bob_inv[0];
|
||||||
|
|
||||||
|
alice.track_repo(&rid, tracking::Scope::Trusted).unwrap();
|
||||||
|
alice.connect_to(&bob);
|
||||||
|
|
||||||
|
// Alice receives Bob's refs.
|
||||||
|
alice.receive(bob.id(), bob.refs_announcement(rid));
|
||||||
|
|
||||||
|
// Alice fetches Bob's refs as this is a new repo.
|
||||||
|
assert_eq!(
|
||||||
|
alice.messages(bob.id()).next(),
|
||||||
|
Some(Message::Fetch { rid })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alice and Bob both have the same repo.
|
||||||
|
///
|
||||||
|
/// First, Alice will not fetch from Bob's `RefsAnnouncement` as Alice does not
|
||||||
|
/// track Bob as `Trusted`.
|
||||||
|
///
|
||||||
|
/// Later Alice tracks Bob, and will be able to fetch Bob's refs.
|
||||||
|
#[test]
|
||||||
|
fn test_refs_announcement_trusted() {
|
||||||
|
logger::init(log::Level::Debug);
|
||||||
|
|
||||||
|
// Create MockStorage for Alice and Bob. Both will have repo with `rid`.
|
||||||
|
let storage_alice = arbitrary::nonempty_storage(1);
|
||||||
|
let rid = *storage_alice.inventory.keys().next().unwrap();
|
||||||
|
let storage_bob = storage_alice.clone();
|
||||||
|
let mut alice = Peer::with_storage("alice", [7, 7, 7, 7], storage_alice);
|
||||||
|
let mut bob = Peer::with_storage("bob", [8, 8, 8, 8], storage_bob);
|
||||||
|
|
||||||
|
// Generate some refs for Bob under their own node_id.
|
||||||
|
let refs = arbitrary::gen::<Refs>(8);
|
||||||
|
let signed_refs = refs.signed(bob.signer()).unwrap();
|
||||||
|
let node_id = bob.id;
|
||||||
|
bob.storage_mut().insert_remote(rid, node_id, signed_refs);
|
||||||
|
|
||||||
|
// Alice uses Scope::Trusted, and did not track Bob yet.
|
||||||
|
alice.connect_to(&bob);
|
||||||
|
alice.track_repo(&rid, tracking::Scope::Trusted).unwrap();
|
||||||
|
|
||||||
|
// Alice receives Bob's refs
|
||||||
|
alice.receive(bob.id(), bob.refs_announcement(rid));
|
||||||
|
|
||||||
|
// Alice does not fetch as Alice is not tracking Bob.
|
||||||
|
assert!(
|
||||||
|
alice.messages(bob.id()).next().is_none(),
|
||||||
|
"Alice is not tracking bob yet."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alice starts to track Bob.
|
||||||
|
let (sender, receiver) = chan::bounded(1);
|
||||||
|
alice.command(Command::TrackNode(bob.id, Some("bob".to_string()), sender));
|
||||||
|
let policy_change = receiver.recv().map_err(runtime::HandleError::from).unwrap();
|
||||||
|
assert!(policy_change);
|
||||||
|
|
||||||
|
// Bob announces refs again.
|
||||||
|
bob.elapse(LocalDuration::from_mins(1)); // Make sure our announcement is fresh.
|
||||||
|
alice.receive(bob.id(), bob.refs_announcement(rid));
|
||||||
|
assert_matches!(alice.messages(bob.id()).next(), Some(Message::Fetch { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_refs_announcement_no_subscribe() {
|
fn test_refs_announcement_no_subscribe() {
|
||||||
let storage = arbitrary::nonempty_storage(1);
|
let storage = arbitrary::nonempty_storage(1);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use radicle_git_ext as git_ext;
|
||||||
use crate::crypto::{Signer, Verified};
|
use crate::crypto::{Signer, Verified};
|
||||||
use crate::identity::doc::{Doc, Id};
|
use crate::identity::doc::{Doc, Id};
|
||||||
use crate::identity::IdentityError;
|
use crate::identity::IdentityError;
|
||||||
|
use crate::node::NodeId;
|
||||||
|
|
||||||
pub use crate::storage::*;
|
pub use crate::storage::*;
|
||||||
|
|
||||||
|
|
@ -15,6 +16,10 @@ pub use crate::storage::*;
|
||||||
pub struct MockStorage {
|
pub struct MockStorage {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub inventory: HashMap<Id, Doc<Verified>>,
|
pub inventory: HashMap<Id, Doc<Verified>>,
|
||||||
|
|
||||||
|
/// All refs keyed by RID.
|
||||||
|
/// Each value is a map of refs keyed by node Id (public key).
|
||||||
|
pub remotes: HashMap<Id, HashMap<NodeId, refs::SignedRefs<Verified>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockStorage {
|
impl MockStorage {
|
||||||
|
|
@ -22,6 +27,7 @@ impl MockStorage {
|
||||||
Self {
|
Self {
|
||||||
path: PathBuf::default(),
|
path: PathBuf::default(),
|
||||||
inventory: inventory.into_iter().collect(),
|
inventory: inventory.into_iter().collect(),
|
||||||
|
remotes: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,8 +35,22 @@ impl MockStorage {
|
||||||
Self {
|
Self {
|
||||||
path: PathBuf::default(),
|
path: PathBuf::default(),
|
||||||
inventory: HashMap::new(),
|
inventory: HashMap::new(),
|
||||||
|
remotes: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a remote `node` with `signed_refs` for the repo `rid`.
|
||||||
|
pub fn insert_remote(
|
||||||
|
&mut self,
|
||||||
|
rid: Id,
|
||||||
|
node: NodeId,
|
||||||
|
signed_refs: refs::SignedRefs<Verified>,
|
||||||
|
) {
|
||||||
|
self.remotes
|
||||||
|
.entry(rid)
|
||||||
|
.or_insert(HashMap::new())
|
||||||
|
.insert(node, signed_refs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadStorage for MockStorage {
|
impl ReadStorage for MockStorage {
|
||||||
|
|
@ -64,6 +84,7 @@ impl ReadStorage for MockStorage {
|
||||||
Ok(MockRepository {
|
Ok(MockRepository {
|
||||||
id: rid,
|
id: rid,
|
||||||
doc: doc.clone(),
|
doc: doc.clone(),
|
||||||
|
remotes: self.remotes.get(&rid).cloned().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +97,7 @@ impl WriteStorage for MockStorage {
|
||||||
Ok(MockRepository {
|
Ok(MockRepository {
|
||||||
id: rid,
|
id: rid,
|
||||||
doc: doc.clone(),
|
doc: doc.clone(),
|
||||||
|
remotes: self.remotes.get(&rid).cloned().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,9 +106,11 @@ impl WriteStorage for MockStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct MockRepository {
|
pub struct MockRepository {
|
||||||
id: Id,
|
id: Id,
|
||||||
doc: Doc<Verified>,
|
doc: Doc<Verified>,
|
||||||
|
remotes: HashMap<NodeId, refs::SignedRefs<Verified>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadRepository for MockRepository {
|
impl ReadRepository for MockRepository {
|
||||||
|
|
@ -95,7 +119,7 @@ impl ReadRepository for MockRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_empty(&self) -> Result<bool, git2::Error> {
|
fn is_empty(&self) -> Result<bool, git2::Error> {
|
||||||
Ok(true)
|
Ok(self.remotes.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> {
|
fn head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> {
|
||||||
|
|
@ -114,12 +138,19 @@ impl ReadRepository for MockRepository {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remote(&self, _remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
|
fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
|
||||||
todo!()
|
self.remotes
|
||||||
|
.get(remote)
|
||||||
|
.map(|refs| Remote::new(*remote, refs.clone()))
|
||||||
|
.ok_or(refs::Error::InvalidRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
|
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
|
||||||
todo!()
|
Ok(self
|
||||||
|
.remotes
|
||||||
|
.iter()
|
||||||
|
.map(|(id, refs)| (*id, Remote::new(*id, refs.clone())))
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self, _oid: Oid) -> Result<git2::Commit, git_ext::Error> {
|
fn commit(&self, _oid: Oid) -> Result<git2::Commit, git_ext::Error> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue