protocol/service: Wire up `FetcherService`

Wire up the new `FetcherService`. This reduces the `Service` fetch
methods down to:
- `fetch`
- `fetched`
- `dequeue_fetches`
- `fetch_refs_at`

This simplifies the code by off-loading the logic to the `fetcher`
family of types, rather than handling the intricacies in the `Service`
itself.

The `Service` now just performs the necessary I/O based on the
returned events from the `fetcher`.

This also removes the fetching state from the node sessions. This
follows the single responsibility, and sessions now only care about
their connectivity.

Breaking changes:
- The `Connected` state of a peer no longer contains fetching
  information, which was being returned as part of the JSON payload
  result.
- The `rad debug` information for ongoing fetches contained the number
  listeners awaiting for results, this was removed.
This commit is contained in:
Fintan Halpenny 2025-08-08 13:13:04 +01:00 committed by Lorenz Leutgeb
parent 9e20890965
commit 1cab036c33
11 changed files with 249 additions and 396 deletions

View File

@ -27,6 +27,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
pushed the default branch to the local user's namespace. The command is now pushed the default branch to the local user's namespace. The command is now
deprecated, and the user should use `git push` instead. deprecated, and the user should use `git push` instead.
## Breaking Changes
- The `Connected` state of a peer no longer contains fetching information. This
information was returned when requesting for `Seeds` on the control socket.
Callers should no longer expect the `fetching` inside that JSON result.
- The `rad debug` information for ongoing fetches contained the number of
subscribers awaiting for results, this was removed.
## 1.6.1 ## 1.6.1
## Fixed Bugs ## Fixed Bugs

View File

@ -350,20 +350,22 @@ impl radicle::node::Handle for Handle {
fn debug(&self) -> Result<serde_json::Value, Self::Error> { fn debug(&self) -> Result<serde_json::Value, Self::Error> {
let (sender, receiver) = chan::bounded(1); let (sender, receiver) = chan::bounded(1);
let query: Arc<QueryState> = Arc::new(move |state| { let query: Arc<QueryState> = Arc::new(move |state| {
let fetcher_state = state.fetching();
let debug = serde_json::json!({ let debug = serde_json::json!({
"outboxSize": state.outbox().len(), "outboxSize": state.outbox().len(),
"fetching": state.fetching().iter().map(|(rid, state)| { "fetching": fetcher_state.active_fetches()
.iter()
.map(|(rid, active)| {
json!({
"rid": rid,
"from": active.from(),
"refsAt": active.refs_at(),
})
}).collect::<Vec<_>>(),
"queue": fetcher_state.queued_fetches().iter().map(|(node, queue)| {
json!({ json!({
"rid": rid, "nid": node,
"from": state.from, "queue": queue.iter().map(|fetch| {
"refsAt": state.refs_at,
"subscribers": state.subscribers.len(),
})
}).collect::<Vec<_>>(),
"queue": state.sessions().values().map(|sess| {
json!({
"nid": sess.id,
"queue": sess.queue.iter().map(|fetch| {
json!({ json!({
"rid": fetch.rid, "rid": fetch.rid,
"from": fetch.from, "from": fetch.from,

View File

@ -1512,6 +1512,7 @@ fn test_queued_fetch_max_capacity() {
// Finish the 1st fetch. // Finish the 1st fetch.
alice.fetched(rid1, bob.id, Ok(fetch::FetchResult::new(doc.clone()))); alice.fetched(rid1, bob.id, Ok(fetch::FetchResult::new(doc.clone())));
// Now the 1st fetch is done, the 2nd fetch is dequeued. // Now the 1st fetch is done, the 2nd fetch is dequeued.
assert_matches!(alice.fetches().next(), Some((rid, _)) if rid == rid2); assert_matches!(alice.fetches().next(), Some((rid, _)) if rid == rid2);
// ... but not the third. // ... but not the third.

View File

@ -280,8 +280,8 @@ impl Default for Config {
/// An active fetch represents a repository being fetched by a particular node. /// An active fetch represents a repository being fetched by a particular node.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActiveFetch { pub struct ActiveFetch {
pub(super) from: NodeId, pub from: NodeId,
pub(super) refs_at: Vec<RefsAt>, pub refs_at: Vec<RefsAt>,
} }
impl ActiveFetch { impl ActiveFetch {

View File

@ -38,6 +38,9 @@ use radicle::storage::refs::SIGREFS_BRANCH;
use radicle::storage::RepositoryError; use radicle::storage::RepositoryError;
use radicle_fetch::policy::SeedingPolicy; use radicle_fetch::policy::SeedingPolicy;
use crate::fetcher;
use crate::fetcher::service::FetcherService;
use crate::fetcher::FetcherState;
use crate::service::gossip::Store as _; use crate::service::gossip::Store as _;
use crate::service::message::{ use crate::service::message::{
Announcement, AnnouncementMessage, Info, NodeAnnouncement, Ping, RefsAnnouncement, RefsStatus, Announcement, AnnouncementMessage, Info, NodeAnnouncement, Ping, RefsAnnouncement, RefsStatus,
@ -303,25 +306,6 @@ pub enum CommandError {
Policy(#[from] policy::Error), Policy(#[from] policy::Error),
} }
/// Error returned by [`Service::try_fetch`].
#[derive(thiserror::Error, Debug)]
enum TryFetchError<'a> {
#[error("ongoing fetch for repository exists")]
AlreadyFetching(&'a mut FetchState),
#[error("peer is not connected; cannot initiate fetch")]
SessionNotConnected,
#[error("peer fetch capacity reached; cannot initiate fetch")]
SessionCapacityReached,
#[error(transparent)]
Namespaces(Box<NamespacesError>),
}
impl From<NamespacesError> for TryFetchError<'_> {
fn from(e: NamespacesError) -> Self {
Self::Namespaces(Box::new(e))
}
}
/// Fetch state for an ongoing fetch. /// Fetch state for an ongoing fetch.
#[derive(Debug)] #[derive(Debug)]
pub struct FetchState { pub struct FetchState {
@ -333,15 +317,6 @@ pub struct FetchState {
pub subscribers: Vec<chan::Sender<FetchResult>>, pub subscribers: Vec<chan::Sender<FetchResult>>,
} }
impl FetchState {
/// Add a subscriber to this fetch.
fn subscribe(&mut self, c: chan::Sender<FetchResult>) {
if !self.subscribers.iter().any(|s| s.same_channel(&c)) {
self.subscribers.push(c);
}
}
}
/// Holds all node stores. /// Holds all node stores.
#[derive(Debug)] #[derive(Debug)]
pub struct Stores<D>(D); pub struct Stores<D>(D);
@ -441,8 +416,7 @@ pub struct Service<D, S, G> {
inventory: InventoryAnnouncement, inventory: InventoryAnnouncement,
/// Source of entropy. /// Source of entropy.
rng: Rng, rng: Rng,
/// Ongoing fetches. fetcher: FetcherService<chan::Sender<FetchResult>>,
fetching: HashMap<RepoId, FetchState>,
/// Request/connection rate limiter. /// Request/connection rate limiter.
limiter: RateLimiter, limiter: RateLimiter,
/// Current seeded repositories bloom filter. /// Current seeded repositories bloom filter.
@ -510,7 +484,15 @@ where
let last_timestamp = node.timestamp; let last_timestamp = node.timestamp;
let clock = LocalTime::default(); // Updated on initialize. let clock = LocalTime::default(); // Updated on initialize.
let inventory = gossip::inventory(clock.into(), []); // Updated on initialize. let inventory = gossip::inventory(clock.into(), []); // Updated on initialize.
let fetcher = {
let config = fetcher::Config::new()
.with_max_concurrency(
std::num::NonZeroUsize::new(config.limits.fetch_concurrency.into())
.expect("fetch concurrency was zero, must be at least 1"),
)
.with_max_capacity(fetcher::MaxQueueSize::default());
FetcherService::new(config)
};
Self { Self {
config, config,
storage, storage,
@ -524,7 +506,7 @@ where
outbox: Outbox::default(), outbox: Outbox::default(),
limiter, limiter,
sessions, sessions,
fetching: HashMap::new(), fetcher,
filter: Filter::empty(), filter: Filter::empty(),
relayed_by: HashMap::default(), relayed_by: HashMap::default(),
last_idle: LocalTime::default(), last_idle: LocalTime::default(),
@ -625,6 +607,10 @@ where
Events::from(self.emitter.subscribe()) Events::from(self.emitter.subscribe())
} }
pub fn fetcher(&self) -> &FetcherState {
self.fetcher.state()
}
/// Get I/O outbox. /// Get I/O outbox.
pub fn outbox(&mut self) -> &mut Outbox { pub fn outbox(&mut self) -> &mut Outbox {
&mut self.outbox &mut self.outbox
@ -900,7 +886,7 @@ where
} }
}, },
Command::Fetch(rid, seed, timeout, resp) => { Command::Fetch(rid, seed, timeout, resp) => {
self.fetch(rid, seed, timeout, Some(resp)); self.fetch(rid, seed, vec![], timeout, Some(resp));
} }
Command::Seed(rid, scope, resp) => { Command::Seed(rid, scope, resp) => {
// Update our seeding policy. // Update our seeding policy.
@ -992,7 +978,8 @@ where
if status.want.is_empty() { if status.want.is_empty() {
debug!(target: "service", "Skipping fetch for {rid}, all refs are already in storage"); debug!(target: "service", "Skipping fetch for {rid}, all refs are already in storage");
} else { } else {
return self._fetch(rid, from, status.want, timeout, channel); self.fetch(rid, from, status.want, timeout, channel);
return true;
} }
} }
Err(e) => { Err(e) => {
@ -1003,247 +990,176 @@ where
false false
} }
/// Initiate an outgoing fetch for some repository.
fn fetch( fn fetch(
&mut self,
rid: RepoId,
from: NodeId,
timeout: time::Duration,
channel: Option<chan::Sender<FetchResult>>,
) -> bool {
self._fetch(rid, from, vec![], timeout, channel)
}
fn _fetch(
&mut self, &mut self,
rid: RepoId, rid: RepoId,
from: NodeId, from: NodeId,
refs_at: Vec<RefsAt>, refs_at: Vec<RefsAt>,
timeout: time::Duration, timeout: time::Duration,
channel: Option<chan::Sender<FetchResult>>, channel: Option<chan::Sender<FetchResult>>,
) -> bool { ) {
match self.try_fetch(rid, &from, refs_at.clone(), timeout) { let session = {
Ok(fetching) => { let reason = format!("peer {from} is not connected; cannot initiate fetch");
let Some(session) = self.sessions.get_mut(&from) else {
if let Some(c) = channel { if let Some(c) = channel {
fetching.subscribe(c); c.send(FetchResult::Failed { reason }).ok();
} }
return true; return;
} };
Err(TryFetchError::AlreadyFetching(fetching)) => { if !session.is_connected() {
// If we're already fetching the same refs from the requested peer, there's nothing
// to do, we simply add the supplied channel to the list of subscribers so that it
// is notified on completion. Otherwise, we queue a fetch with the requested peer.
if fetching.from == from && fetching.refs_at == refs_at {
debug!(target: "service", "Ignoring redundant fetch of {rid} from {from}");
if let Some(c) = channel {
fetching.subscribe(c);
}
} else {
let fetch = QueuedFetch {
rid,
refs_at,
from,
timeout,
channel,
};
debug!(target: "service", "Queueing fetch for {rid} with {from} (already fetching)..");
self.queue_fetch(fetch);
}
}
Err(TryFetchError::SessionCapacityReached) => {
debug!(target: "service", "Fetch capacity reached for {from}, queueing {rid}..");
self.queue_fetch(QueuedFetch {
rid,
refs_at,
from,
timeout,
channel,
});
}
Err(e) => {
if let Some(c) = channel { if let Some(c) = channel {
c.send(FetchResult::Failed { c.send(FetchResult::Failed { reason }).ok();
reason: e.to_string(),
})
.ok();
} }
return;
} }
} session
false
}
fn queue_fetch(&mut self, fetch: QueuedFetch) {
let Some(s) = self.sessions.get_mut(&fetch.from) else {
log::debug!(target: "service", "Cannot queue fetch for unknown session {}", fetch.from);
return;
}; };
if let Err(e) = s.queue_fetch(fetch) {
let fetch = e.inner();
log::debug!(target: "service", "Unable to queue fetch for {} with {}: {e}", &fetch.rid, &fetch.from);
}
}
// TODO: Buffer/throttle fetches. let cmd = fetcher::state::command::Fetch {
fn try_fetch(
&mut self,
rid: RepoId,
from: &NodeId,
refs_at: Vec<RefsAt>,
timeout: time::Duration,
) -> Result<&mut FetchState, TryFetchError<'_>> {
let from = *from;
let Some(session) = self.sessions.get_mut(&from) else {
return Err(TryFetchError::SessionNotConnected);
};
let fetching = self.fetching.entry(rid);
trace!(target: "service", "Trying to fetch {refs_at:?} for {rid}..");
let fetching = match fetching {
Entry::Vacant(fetching) => fetching,
Entry::Occupied(fetching) => {
// We're already fetching this repo from some peer.
return Err(TryFetchError::AlreadyFetching(fetching.into_mut()));
}
};
// Sanity check: We shouldn't be fetching from this session, since we return above if we're
// fetching from any session.
debug_assert!(!session.is_fetching(&rid));
if !session.is_connected() {
// This can happen if a session disconnects in the time between asking for seeds to
// fetch from, and initiating the fetch from one of those seeds.
return Err(TryFetchError::SessionNotConnected);
}
if session.is_at_capacity() {
// If we're already fetching multiple repos from this peer.
return Err(TryFetchError::SessionCapacityReached);
}
let fetching = fetching.insert(FetchState {
from, from,
refs_at: refs_at.clone(),
subscribers: vec![],
});
self.outbox.fetch(
session,
rid, rid,
refs_at, refs_at,
timeout, timeout,
self.config.limits.fetch_pack_receive, };
); let fetcher::service::FetchInitiated { event, rejected } = self.fetcher.fetch(cmd, channel);
Ok(fetching) if let Some(c) = rejected {
c.send(FetchResult::Failed {
reason: "fetch queue at capacity".to_string(),
})
.ok();
}
match event {
fetcher::state::event::Fetch::Started {
rid,
from,
refs_at,
timeout,
} => {
debug!(target: "service", "Starting fetch for {rid} from {from}");
self.outbox.fetch(
session,
rid,
refs_at,
timeout,
self.config.limits.fetch_pack_receive,
);
}
fetcher::state::event::Fetch::Queued { rid, from } => {
debug!(target: "service", "Queued fetch for {rid} from {from}");
}
fetcher::state::event::Fetch::AlreadyFetching { rid, from } => {
debug!(target: "service", "Already fetching {rid} from {from}");
}
fetcher::state::event::Fetch::QueueAtCapacity { rid, from, .. } => {
debug!(target: "service", "Queue at capacity for {from}, rejected {rid}");
}
}
} }
pub fn fetched( pub fn fetched(
&mut self, &mut self,
rid: RepoId, rid: RepoId,
remote: NodeId, from: NodeId,
result: Result<crate::worker::fetch::FetchResult, crate::worker::FetchError>, result: Result<crate::worker::fetch::FetchResult, crate::worker::FetchError>,
) { ) {
let Some(fetching) = self.fetching.remove(&rid) else { let cmd = fetcher::state::command::Fetched { from, rid };
debug!(target: "service", "Received unexpected fetch result for {rid}, from {remote}"); let fetcher::service::FetchCompleted { event, subscribers } = self.fetcher.fetched(cmd);
return;
};
debug_assert_eq!(fetching.from, remote);
if let Some(s) = self.sessions.get_mut(&remote) { // Dequeue next fetches
// Mark this RID as fetched for this session.
s.fetched(rid);
}
// Notify all fetch subscribers of the fetch result. This is used when the user requests
// a fetch via the CLI, for example.
for sub in &fetching.subscribers {
debug!(target: "service", "Found existing fetch request from {remote}, sending result..");
let result = match &result {
Ok(success) => FetchResult::Success {
updated: success.updated.clone(),
namespaces: success.namespaces.clone(),
clone: success.clone,
},
Err(e) => FetchResult::Failed {
reason: e.to_string(),
},
};
if sub.send(result).is_err() {
debug!(target: "service", "Failed to send fetch result for {rid} from {remote}..");
} else {
debug!(target: "service", "Sent fetch result for {rid} from {remote}..");
}
}
match result {
Ok(crate::worker::fetch::FetchResult {
updated,
canonical,
namespaces,
clone,
doc,
}) => {
info!(target: "service", "Fetched {rid} from {remote} successfully");
// Update our routing table in case this fetch was user-initiated and doesn't
// come from an announcement.
self.seed_discovered(rid, remote, self.clock.into());
for update in &updated {
if update.is_skipped() {
trace!(target: "service", "Ref skipped: {update} for {rid}");
} else {
debug!(target: "service", "Ref updated: {update} for {rid}");
}
}
self.emitter.emit(Event::RefsFetched {
remote,
rid,
updated: updated.clone(),
});
self.emitter
.emit_all(canonical.into_iter().map(|(refname, target)| {
Event::CanonicalRefUpdated {
rid,
refname,
target,
}
}));
// Announce our new inventory if this fetch was a full clone.
// Only update and announce inventory for public repositories.
if clone && doc.is_public() {
debug!(target: "service", "Updating and announcing inventory for cloned repository {rid}..");
if let Err(e) = self.add_inventory(rid) {
warn!(target: "service", "Failed to announce inventory for {rid}: {e}");
}
}
// It's possible for a fetch to succeed but nothing was updated.
if updated.is_empty() || updated.iter().all(|u| u.is_skipped()) {
debug!(target: "service", "Nothing to announce, no refs were updated..");
} else {
// Finally, announce the refs. This is useful for nodes to know what we've synced,
// beyond just knowing that we have added an item to our inventory.
if let Err(e) = self.announce_refs(rid, doc.into(), namespaces, false) {
warn!(target: "service", "Failed to announce new refs: {e}");
}
}
}
Err(err) => {
warn!(target: "service", "Fetch failed for {rid} from {remote}: {err}");
// For now, we only disconnect the remote in case of timeout. In the future,
// there may be other reasons to disconnect.
if err.is_timeout() {
self.outbox.disconnect(remote, DisconnectReason::Fetch(err));
}
}
}
// We can now try to dequeue more fetches.
self.dequeue_fetches(); self.dequeue_fetches();
match event {
fetcher::state::event::Fetched::NotFound { from, rid } => {
debug!(target: "service", "Unexpected fetch result for {rid} from {from}");
}
fetcher::state::event::Fetched::Completed {
from,
rid,
refs_at: _,
} => {
// Notify responders
let fetch_result = match &result {
Ok(success) => FetchResult::Success {
updated: success.updated.clone(),
namespaces: success.namespaces.clone(),
clone: success.clone,
},
Err(e) => FetchResult::Failed {
reason: e.to_string(),
},
};
for responder in subscribers {
responder.send(fetch_result.clone()).ok();
}
match result {
Ok(crate::worker::fetch::FetchResult {
updated,
canonical,
namespaces,
clone,
doc,
}) => {
info!(target: "service", "Fetched {rid} from {from} successfully");
// Update our routing table in case this fetch was user-initiated and doesn't
// come from an announcement.
self.seed_discovered(rid, from, self.clock.into());
for update in &updated {
if update.is_skipped() {
trace!(target: "service", "Ref skipped: {update} for {rid}");
} else {
debug!(target: "service", "Ref updated: {update} for {rid}");
}
}
self.emitter.emit(Event::RefsFetched {
remote: from,
rid,
updated: updated.clone(),
});
self.emitter
.emit_all(canonical.into_iter().map(|(refname, target)| {
Event::CanonicalRefUpdated {
rid,
refname,
target,
}
}));
// Announce our new inventory if this fetch was a full clone.
// Only update and announce inventory for public repositories.
if clone && doc.is_public() {
debug!(target: "service", "Updating and announcing inventory for cloned repository {rid}..");
if let Err(e) = self.add_inventory(rid) {
warn!(target: "service", "Failed to announce inventory for {rid}: {e}");
}
}
// It's possible for a fetch to succeed but nothing was updated.
if updated.is_empty() || updated.iter().all(|u| u.is_skipped()) {
debug!(target: "service", "Nothing to announce, no refs were updated..");
} else {
// Finally, announce the refs. This is useful for nodes to know what we've synced,
// beyond just knowing that we have added an item to our inventory.
if let Err(e) = self.announce_refs(rid, doc.into(), namespaces, false) {
warn!(target: "service", "Failed to announce new refs: {e}");
}
}
}
Err(err) => {
warn!(target: "service", "Fetch failed for {rid} from {from}: {err}");
// For now, we only disconnect the from in case of timeout. In the future,
// there may be other reasons to disconnect.
if err.is_timeout() {
self.outbox.disconnect(from, DisconnectReason::Fetch(err));
}
}
}
}
}
} }
/// Attempt to dequeue fetches from all peers. /// Attempt to dequeue fetches from all peers.
@ -1260,38 +1176,42 @@ where
.map(|(k, _)| *k) .map(|(k, _)| *k)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Try to dequeue once per session.
for nid in sessions { for nid in sessions {
// SAFETY: All the keys we are iterating on exist.
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let sess = self.sessions.get_mut(&nid).unwrap(); let sess = self.sessions.get_mut(&nid).unwrap();
if !sess.is_connected() || sess.is_at_capacity() { if !sess.is_connected() {
continue; continue;
} }
if let Some(QueuedFetch { let Some(fetcher::QueuedFetch {
rid, rid,
from, from,
refs_at, refs_at,
timeout, timeout,
channel, }) = self.fetcher.dequeue(&nid)
}) = sess.dequeue_fetch() else {
{ continue;
debug!(target: "service", "Dequeued fetch for {rid} from session {from}.."); };
if let Some(refs) = NonEmpty::from_vec(refs_at) { // Check seeding policy
let repo_entry = self.policies.seed_policy(&rid).expect( let repo_entry = self
"Service::dequeue_fetch: error accessing repo seeding configuration", .policies
); .seed_policy(&rid)
let SeedingPolicy::Allow { scope } = repo_entry.policy else { .expect("error accessing repo seeding configuration");
debug!(target: "service", "Repository {rid} is no longer seeded, skipping..");
continue; let SeedingPolicy::Allow { scope } = repo_entry.policy else {
}; debug!(target: "service", "Repository {} no longer seeded, skipping", rid);
self.fetch_refs_at(rid, from, refs, scope, timeout, channel); continue;
} else { };
// If no refs are specified, always do a full fetch.
self.fetch(rid, from, timeout, channel); debug!(target: "service", "Dequeued fetch for {} from {}", rid, from);
}
// Channel is `None` in both cases since they will already be
// registered with the fetcher service.
if let Some(refs) = NonEmpty::from_vec(refs_at.clone()) {
self.fetch_refs_at(rid, from, refs, scope, timeout, None);
} else {
self.fetch(rid, from, refs_at, timeout, None);
} }
} }
} }
@ -1393,7 +1313,6 @@ where
self.config.is_persistent(&remote), self.config.is_persistent(&remote),
self.rng.clone(), self.rng.clone(),
self.clock, self.clock,
self.config.limits.clone(),
)); ));
self.outbox.write_all(peer, msgs); self.outbox.write_all(peer, msgs);
} }
@ -1424,19 +1343,30 @@ where
let link = session.link; let link = session.link;
let addr = session.addr.clone(); let addr = session.addr.clone();
self.fetching.retain(|_, fetching| { let cmd = fetcher::state::command::Cancel { from: remote };
if fetching.from != remote { let fetcher::service::FetchesCancelled { event, orphaned } = self.fetcher.cancel(cmd);
return true;
match event {
fetcher::state::event::Cancel::Unexpected { from } => {
debug!(target: "service", "No fetches to cancel for {from}");
} }
// Remove and fail any pending fetches from this remote node. fetcher::state::event::Cancel::Canceled {
for resp in &fetching.subscribers { from,
resp.send(FetchResult::Failed { active,
reason: format!("disconnected: {reason}"), queued,
} => {
debug!(target: "service", "Cancelled {} ongoing, {} queued for {from}", active.len(), queued.len());
}
}
// Notify orphaned responders
for (rid, responder) in orphaned {
responder
.send(FetchResult::Failed {
reason: format!("failed fetch to {rid}, peer disconnected: {reason}"),
}) })
.ok(); .ok();
} }
false
});
// Attempt to re-connect to persistent peers. // Attempt to re-connect to persistent peers.
if self.config.is_persistent(&remote) { if self.config.is_persistent(&remote) {
@ -1653,7 +1583,7 @@ where
for rid in missing { for rid in missing {
debug!(target: "service", "Missing seeded inventory {rid}; initiating fetch.."); debug!(target: "service", "Missing seeded inventory {rid}; initiating fetch..");
self.fetch(rid, *announcer, FETCH_TIMEOUT, None); self.fetch(rid, *announcer, vec![], FETCH_TIMEOUT, None);
} }
return Ok(relay); return Ok(relay);
} }
@ -2286,13 +2216,7 @@ where
} }
self.sessions.insert( self.sessions.insert(
nid, nid,
Session::outbound( Session::outbound(nid, addr.clone(), persistent, self.rng.clone()),
nid,
addr.clone(),
persistent,
self.rng.clone(),
self.config.limits.clone(),
),
); );
self.outbox.connect(nid, addr); self.outbox.connect(nid, addr);
@ -2565,7 +2489,7 @@ where
Ok(seeds) => { Ok(seeds) => {
if let Some(connected) = NonEmpty::from_vec(seeds.connected().collect()) { if let Some(connected) = NonEmpty::from_vec(seeds.connected().collect()) {
for seed in connected { for seed in connected {
self.fetch(rid, seed.nid, FETCH_TIMEOUT, None); self.fetch(rid, seed.nid, vec![], FETCH_TIMEOUT, None);
} }
} else { } else {
// TODO: We should make sure that this fetch is retried later, either // TODO: We should make sure that this fetch is retried later, either
@ -2719,7 +2643,7 @@ pub trait ServiceState {
/// Get the existing sessions. /// Get the existing sessions.
fn sessions(&self) -> &Sessions; fn sessions(&self) -> &Sessions;
/// Get fetch state. /// Get fetch state.
fn fetching(&self) -> &HashMap<RepoId, FetchState>; fn fetching(&self) -> &FetcherState;
/// Get outbox. /// Get outbox.
fn outbox(&self) -> &Outbox; fn outbox(&self) -> &Outbox;
/// Get rate limiter. /// Get rate limiter.
@ -2752,8 +2676,8 @@ where
&self.sessions &self.sessions
} }
fn fetching(&self) -> &HashMap<RepoId, FetchState> { fn fetching(&self) -> &FetcherState {
&self.fetching self.fetcher.state()
} }
fn outbox(&self) -> &Outbox { fn outbox(&self) -> &Outbox {

View File

@ -138,8 +138,6 @@ impl Outbox {
timeout: time::Duration, timeout: time::Duration,
reader_limit: FetchPackSizeLimit, reader_limit: FetchPackSizeLimit,
) { ) {
peer.fetching(rid);
let refs_at = (!refs_at.is_empty()).then_some(refs_at); let refs_at = (!refs_at.is_empty()).then_some(refs_at);
if let Some(refs_at) = &refs_at { if let Some(refs_at) = &refs_at {

View File

@ -1,8 +1,7 @@
use std::collections::{HashSet, VecDeque}; use std::collections::VecDeque;
use std::{fmt, time}; use std::{fmt, time};
use crossbeam_channel as chan; use crossbeam_channel as chan;
use radicle::node::config::Limits;
use radicle::node::{FetchResult, Severity}; use radicle::node::{FetchResult, Severity};
use radicle::node::{Link, Timestamp}; use radicle::node::{Link, Timestamp};
pub use radicle::node::{PingState, State}; pub use radicle::node::{PingState, State};
@ -111,8 +110,6 @@ pub struct Session {
pub subscribe: Option<message::Subscribe>, pub subscribe: Option<message::Subscribe>,
/// Last time a message was received from the peer. /// Last time a message was received from the peer.
pub last_active: LocalTime, pub last_active: LocalTime,
/// Fetch queue.
pub queue: VecDeque<QueuedFetch>,
/// Connection attempts. For persistent peers, Tracks /// Connection attempts. For persistent peers, Tracks
/// how many times we've attempted to connect. We reset this to zero /// how many times we've attempted to connect. We reset this to zero
@ -120,8 +117,6 @@ pub struct Session {
attempts: usize, attempts: usize,
/// Source of entropy. /// Source of entropy.
rng: Rng, rng: Rng,
/// Protocol limits.
limits: Limits,
} }
impl fmt::Display for Session { impl fmt::Display for Session {
@ -159,7 +154,7 @@ impl From<&Session> for radicle::node::Session {
} }
impl Session { impl Session {
pub fn outbound(id: NodeId, addr: Address, persistent: bool, rng: Rng, limits: Limits) -> Self { pub fn outbound(id: NodeId, addr: Address, persistent: bool, rng: Rng) -> Self {
Self { Self {
id, id,
addr, addr,
@ -168,28 +163,18 @@ impl Session {
subscribe: None, subscribe: None,
persistent, persistent,
last_active: LocalTime::default(), last_active: LocalTime::default(),
queue: VecDeque::with_capacity(MAX_FETCH_QUEUE_SIZE),
attempts: 1, attempts: 1,
rng, rng,
limits,
} }
} }
pub fn inbound( pub fn inbound(id: NodeId, addr: Address, persistent: bool, rng: Rng, time: LocalTime) -> Self {
id: NodeId,
addr: Address,
persistent: bool,
rng: Rng,
time: LocalTime,
limits: Limits,
) -> Self {
Self { Self {
id, id,
addr, addr,
state: State::Connected { state: State::Connected {
since: time, since: time,
ping: PingState::default(), ping: PingState::default(),
fetching: HashSet::default(),
latencies: VecDeque::default(), latencies: VecDeque::default(),
stable: false, stable: false,
}, },
@ -197,10 +182,8 @@ impl Session {
subscribe: None, subscribe: None,
persistent, persistent,
last_active: time, last_active: time,
queue: VecDeque::new(),
attempts: 0, attempts: 0,
rng, rng,
limits,
} }
} }
@ -224,41 +207,6 @@ impl Session {
matches!(self.state, State::Initial) matches!(self.state, State::Initial)
} }
pub fn is_at_capacity(&self) -> bool {
if let State::Connected { fetching, .. } = &self.state {
if fetching.len() >= self.limits.fetch_concurrency.into() {
return true;
}
}
false
}
pub fn is_fetching(&self, rid: &RepoId) -> bool {
if let State::Connected { fetching, .. } = &self.state {
return fetching.contains(rid);
}
false
}
/// Queue a fetch. Returns `true` if it was added to the queue, and `false` if
/// it already was present in the queue.
pub fn queue_fetch(&mut self, fetch: QueuedFetch) -> Result<(), QueueError> {
assert_eq!(fetch.from, self.id);
if self.queue.len() >= MAX_FETCH_QUEUE_SIZE {
return Err(QueueError::CapacityReached(fetch));
} else if self.queue.contains(&fetch) {
return Err(QueueError::Duplicate(fetch));
}
self.queue.push_back(fetch);
Ok(())
}
pub fn dequeue_fetch(&mut self) -> Option<QueuedFetch> {
self.queue.pop_front()
}
pub fn attempts(&self) -> usize { pub fn attempts(&self) -> usize {
self.attempts self.attempts
} }
@ -279,33 +227,6 @@ impl Session {
} }
} }
/// Mark this session as fetching the given RID.
///
/// # Panics
///
/// If it is already fetching that RID, or the session is disconnected.
pub fn fetching(&mut self, rid: RepoId) {
if let State::Connected { fetching, .. } = &mut self.state {
assert!(
fetching.insert(rid),
"Session must not already be fetching {rid}"
);
} else {
panic!(
"Attempting to fetch {rid} from disconnected session {}",
self.id
);
}
}
pub fn fetched(&mut self, rid: RepoId) {
if let State::Connected { fetching, .. } = &mut self.state {
if !fetching.remove(&rid) {
log::debug!(target: "service", "Fetched unknown repository {rid}");
}
}
}
pub fn to_attempted(&mut self) { pub fn to_attempted(&mut self) {
assert!( assert!(
self.is_initial(), self.is_initial(),
@ -324,7 +245,6 @@ impl Session {
self.state = State::Connected { self.state = State::Connected {
since, since,
ping: PingState::default(), ping: PingState::default(),
fetching: HashSet::default(),
latencies: VecDeque::default(), latencies: VecDeque::default(),
stable: false, stable: false,
}; };

View File

@ -87,7 +87,7 @@ fn print_schema() -> io::Result<()> {
#[schemars(with = "radicle::schemars_ext::crypto::PublicKey")] #[schemars(with = "radicle::schemars_ext::crypto::PublicKey")]
radicle::node::NodeId, radicle::node::NodeId,
), ),
Config(radicle::node::Config), Config(Box<radicle::node::Config>),
ListenAddrs(ListenAddrs), ListenAddrs(ListenAddrs),
ConnectResult(radicle::node::ConnectResult), ConnectResult(radicle::node::ConnectResult),
Success(radicle::node::Success), Success(radicle::node::Success),

View File

@ -46,6 +46,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- The data returned by `Seeds` contains `state`, which in turn contained the
field `fetching` for ongoing fetches of that node, if in the `Connected`
state. `Connected` no longer contains that field.
### Security ### Security
## 0.20.0 ## 0.20.0

View File

@ -107,8 +107,6 @@ pub enum State {
/// Ping state. /// Ping state.
#[serde(skip)] #[serde(skip)]
ping: PingState, ping: PingState,
/// Ongoing fetches.
fetching: HashSet<RepoId>,
/// Measured latencies for this peer. /// Measured latencies for this peer.
#[serde(skip)] #[serde(skip)]
latencies: VecDeque<LocalDuration>, latencies: VecDeque<LocalDuration>,
@ -696,7 +694,7 @@ impl From<Vec<Seed>> for Seeds {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "camelCase")] #[serde(tag = "status", rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum FetchResult { pub enum FetchResult {
@ -1486,7 +1484,6 @@ mod test {
&serde_json::to_string(&CommandResult::Okay(State::Connected { &serde_json::to_string(&CommandResult::Okay(State::Connected {
since: LocalTime::now(), since: LocalTime::now(),
ping: Default::default(), ping: Default::default(),
fetching: Default::default(),
latencies: VecDeque::default(), latencies: VecDeque::default(),
stable: false, stable: false,
})) }))

View File

@ -313,7 +313,6 @@ mod test {
&serde_json::to_string(&CommandResult::Okay(State::Connected { &serde_json::to_string(&CommandResult::Okay(State::Connected {
since: LocalTime::now(), since: LocalTime::now(),
ping: Default::default(), ping: Default::default(),
fetching: Default::default(),
latencies: VecDeque::default(), latencies: VecDeque::default(),
stable: false, stable: false,
})) }))
@ -329,7 +328,7 @@ mod test {
); );
assert_matches!( assert_matches!(
json::from_str::<CommandResult<Seeds>>( json::from_str::<CommandResult<Seeds>>(
r#"[{"nid":"z6MksmpU5b1dS7oaqF2bHXhQi1DWy2hB7Mh9CuN7y1DN6QSz","addrs":[{"addr":"seed.radicle.example.com:8776","source":"peer","lastSuccess":1699983994234,"lastAttempt":1699983994000,"banned":false}],"state":{"connected":{"since":1699983994,"fetching":[]}}}]"# r#"[{"nid":"z6MksmpU5b1dS7oaqF2bHXhQi1DWy2hB7Mh9CuN7y1DN6QSz","addrs":[{"addr":"seed.radicle.example.com:8776","source":"peer","lastSuccess":1699983994234,"lastAttempt":1699983994000,"banned":false}],"state":{"connected":{"since":1699983994}}}]"#
), ),
Ok(CommandResult::Okay(_)) Ok(CommandResult::Okay(_))
); );