node: Replication fixes

* Read git request line before starting daemon, so that we don't leave
  git data in the socket read buffer in case of an error connecting to
  the daemon.
* Send a response on the fetch channel even if we disconnect the remote.
* Don't send `done` packet if there's an error; it won't be expected.
This commit is contained in:
Alexis Sellier 2023-02-07 21:04:13 +01:00
parent 803d36f0f6
commit f66ecd4ece
No known key found for this signature in database
4 changed files with 64 additions and 71 deletions

View File

@ -26,7 +26,7 @@ use crate::crypto;
use crate::crypto::{Signer, Verified}; use crate::crypto::{Signer, Verified};
use crate::identity::{Doc, Id}; use crate::identity::{Doc, Id};
use crate::node; use crate::node;
use crate::node::{Address, Features}; use crate::node::{Address, Features, FetchResult};
use crate::prelude::*; use crate::prelude::*;
use crate::service::message::{Announcement, AnnouncementMessage, Ping}; use crate::service::message::{Announcement, AnnouncementMessage, Ping};
use crate::service::message::{NodeAnnouncement, RefsAnnouncement}; use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
@ -34,7 +34,6 @@ use crate::service::session::Protocol;
use crate::storage; use crate::storage;
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteStorage}; use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteStorage};
use crate::storage::{Namespaces, ReadStorage}; use crate::storage::{Namespaces, ReadStorage};
use crate::worker;
use crate::worker::FetchError; use crate::worker::FetchError;
use crate::Link; use crate::Link;
@ -109,7 +108,7 @@ pub enum Command {
/// Lookup seeds for the given repository in the routing table. /// Lookup seeds for the given repository in the routing table.
Seeds(Id, chan::Sender<Vec<NodeId>>), Seeds(Id, chan::Sender<Vec<NodeId>>),
/// Fetch the given repository from the network. /// Fetch the given repository from the network.
Fetch(Id, NodeId, chan::Sender<node::FetchResult>), Fetch(Id, NodeId, chan::Sender<FetchResult>),
/// Track the given repository. /// Track the given repository.
TrackRepo(Id, chan::Sender<bool>), TrackRepo(Id, chan::Sender<bool>),
/// Untrack the given repository. /// Untrack the given repository.
@ -176,7 +175,7 @@ pub struct Service<R, A, S, G> {
/// Whether our local inventory no long represents what we have announced to the network. /// Whether our local inventory no long represents what we have announced to the network.
out_of_sync: bool, out_of_sync: bool,
/// Fetch requests initiated by user, which are waiting for results. /// Fetch requests initiated by user, which are waiting for results.
fetch_reqs: HashMap<Id, chan::Sender<node::FetchResult>>, fetch_reqs: HashMap<Id, chan::Sender<FetchResult>>,
/// Current tracked repository bloom filter. /// Current tracked repository bloom filter.
filter: Filter, filter: Filter,
/// Last time the service was idle. /// Last time the service was idle.
@ -488,43 +487,45 @@ where
} }
} }
pub fn fetched(&mut self, result: worker::FetchResult) { pub fn fetched(&mut self, fetch: Fetch, result: Result<Vec<RefUpdate>, FetchError>) {
let remote = result.fetch.remote; let remote = fetch.remote;
let rid = result.fetch.rid; let rid = fetch.rid;
let initiated = result.fetch.initiated; let initiated = fetch.initiated;
if initiated { if initiated {
log::debug!( let result = match result {
target: "service",
"Fetched {rid} {remote} (error={:?})", result.result.as_ref().err()
);
let result = match result.result {
Ok(updated) => { Ok(updated) => {
log::debug!(target: "service", "Fetched {rid} from {remote}");
self.reactor.event(Event::RefsFetched { self.reactor.event(Event::RefsFetched {
remote, remote,
rid, rid,
updated: updated.clone(), updated: updated.clone(),
}); });
Ok(updated) FetchResult::Success { updated }
} }
Err(err) => { Err(err) => {
error!(target: "service", "Fetch failed for {rid} from {remote}: {err}"); let reason = err.to_string();
error!(target: "service", "Fetch failed for {rid} from {remote}: {reason}");
if let FetchError::Io(_) = 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.reactor self.reactor
.disconnect(remote, DisconnectReason::Fetch(err)); .disconnect(remote, DisconnectReason::Fetch(err));
return;
} else {
Err(err)
} }
FetchResult::Failed { reason }
} }
}; };
if let Some(results) = self.fetch_reqs.get(&rid) { if let Some(results) = self.fetch_reqs.get(&rid) {
log::debug!(target: "service", "Found existing fetch request, sending result.."); log::debug!(target: "service", "Found existing fetch request, sending result..");
if results.send(node::FetchResult::from(result)).is_err() { if results.send(result).is_err() {
log::error!(target: "service", "Error sending fetch result for {rid}.."); log::error!(target: "service", "Error sending fetch result for {rid}..");
// FIXME: We should remove the channel even on success, once all seeds
// were fetched from. Otherwise an organic fetch will try to send on the
// channel.
self.fetch_reqs.remove(&rid); self.fetch_reqs.remove(&rid);
} else { } else {
log::debug!(target: "service", "Sent fetch result for {rid}.."); log::debug!(target: "service", "Sent fetch result for {rid}..");

View File

@ -16,11 +16,11 @@ use crate::crypto::Signer;
use crate::git::raw as git; use crate::git::raw as git;
use crate::prelude::Address; use crate::prelude::Address;
use crate::service::reactor::Io; use crate::service::reactor::Io;
use crate::service::{DisconnectReason, Event, Message, NodeId}; use crate::service::{DisconnectReason, Event, Fetch, Message, NodeId};
use crate::storage::{Namespaces, RefUpdate}; use crate::storage::{Namespaces, RefUpdate};
use crate::storage::{WriteRepository, WriteStorage}; use crate::storage::{WriteRepository, WriteStorage};
use crate::test::peer::Service; use crate::test::peer::Service;
use crate::worker::{FetchError, FetchResult}; use crate::worker::FetchError;
use crate::Link; use crate::Link;
/// Minimum latency between peers. /// Minimum latency between peers.
@ -63,7 +63,7 @@ pub enum Input {
/// Received a message from a remote peer. /// Received a message from a remote peer.
Received(NodeId, Vec<Message>), Received(NodeId, Vec<Message>),
/// Fetch completed for a node. /// Fetch completed for a node.
Fetched(Arc<FetchResult>), Fetched(Fetch, Rc<Result<Vec<RefUpdate>, FetchError>>),
/// Used to advance the state machine after some wall time has passed. /// Used to advance the state machine after some wall time has passed.
Wake, Wake,
} }
@ -108,11 +108,11 @@ impl fmt::Display for Scheduled {
Input::Wake => { Input::Wake => {
write!(f, "{}: Tock", self.node) write!(f, "{}: Tock", self.node)
} }
Input::Fetched(result) => { Input::Fetched(fetch, _) => {
write!( write!(
f, f,
"{} <~ {} ({}): FetchCompleted", "{} <~ {} ({}): Fetched",
self.node, result.fetch.remote, result.fetch.rid self.node, fetch.remote, fetch.rid
) )
} }
} }
@ -411,17 +411,13 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
p.received_message(id, msg); p.received_message(id, msg);
} }
} }
Input::Fetched(result) => { Input::Fetched(f, result) => {
let result = Arc::try_unwrap(result).unwrap(); let result = Rc::try_unwrap(result).unwrap();
let mut repo = p.storage().repository(result.fetch.rid).unwrap(); let mut repo = p.storage().repository(f.rid).unwrap();
fetch( fetch(&mut repo, &f.remote, f.namespaces.clone()).unwrap();
&mut repo,
&result.fetch.remote, p.fetched(f, result);
result.fetch.namespaces.clone(),
)
.unwrap();
p.fetched(result);
} }
} }
for o in p.by_ref() { for o in p.by_ref() {
@ -621,10 +617,10 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
Scheduled { Scheduled {
node, node,
remote: fetch.remote, remote: fetch.remote,
input: Input::Fetched(Arc::new(FetchResult { input: Input::Fetched(
fetch, fetch,
result: Err(FetchError::Io(io::ErrorKind::Other.into())), Rc::new(Err(FetchError::Io(io::ErrorKind::Other.into()))),
})), ),
}, },
); );
} else { } else {
@ -633,10 +629,7 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
Scheduled { Scheduled {
node, node,
remote: fetch.remote, remote: fetch.remote,
input: Input::Fetched(Arc::new(FetchResult { input: Input::Fetched(fetch, Rc::new(Ok(vec![]))),
fetch,
result: Ok(vec![]),
})),
}, },
); );
} }

View File

@ -354,7 +354,7 @@ where
peer.downgrade(); peer.downgrade();
self.actions.push_back(Action::RegisterTransport(session)); self.actions.push_back(Action::RegisterTransport(session));
self.service.fetched(task.result); self.service.fetched(task.fetch, task.result);
} }
} }

View File

@ -1,5 +1,4 @@
use std::io::{prelude::*, BufReader}; use std::io::{prelude::*, BufReader};
use std::ops::Deref;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use std::{env, io, net, process, thread, time}; use std::{env, io, net, process, thread, time};
@ -35,21 +34,6 @@ pub struct Config {
pub storage: Storage, pub storage: Storage,
} }
/// Result of a fetch request from a specific seed.
#[derive(Debug)]
pub struct FetchResult {
pub fetch: Fetch,
pub result: Result<Vec<RefUpdate>, FetchError>,
}
impl Deref for FetchResult {
type Target = Result<Vec<RefUpdate>, FetchError>;
fn deref(&self) -> &Self::Target {
&self.result
}
}
/// Error returned by fetch. /// Error returned by fetch.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum FetchError { pub enum FetchError {
@ -65,6 +49,13 @@ pub enum FetchError {
Project(#[from] storage::ProjectError), Project(#[from] storage::ProjectError),
} }
impl FetchError {
/// Check if it's a timeout error.
pub fn is_timeout(&self) -> bool {
matches!(self, FetchError::Io(e) if e.kind() == io::ErrorKind::TimedOut)
}
}
/// Task to be accomplished on a worker thread. /// Task to be accomplished on a worker thread.
/// This is either going to be an outgoing or incoming fetch. /// This is either going to be an outgoing or incoming fetch.
pub struct Task<G: Signer + EcSign> { pub struct Task<G: Signer + EcSign> {
@ -75,7 +66,8 @@ pub struct Task<G: Signer + EcSign> {
/// Worker response. /// Worker response.
pub struct TaskResult<G: Signer + EcSign> { pub struct TaskResult<G: Signer + EcSign> {
pub result: FetchResult, pub fetch: Fetch,
pub result: Result<Vec<RefUpdate>, FetchError>,
pub session: WireSession<G>, pub session: WireSession<G>,
} }
@ -108,12 +100,15 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
} = task; } = task;
let (session, result) = self._process(&fetch, drain, session); let (session, result) = self._process(&fetch, drain, session);
let result = FetchResult { fetch, result };
log::debug!(target: "worker", "Sending response back to service.."); log::debug!(target: "worker", "Sending response back to service..");
if self if self
.handle .handle
.worker_result(TaskResult { result, session }) .worker_result(TaskResult {
fetch,
result,
session,
})
.is_err() .is_err()
{ {
log::error!(target: "worker", "Unable to report fetch result: worker channel disconnected"); log::error!(target: "worker", "Unable to report fetch result: worker channel disconnected");
@ -136,11 +131,12 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
let result = self.fetch(fetch, &mut tunnel); let result = self.fetch(fetch, &mut tunnel);
let mut session = tunnel.into_session(); let mut session = tunnel.into_session();
if let Err(err) = pktline::done(&mut session) { // If there are no errors, send a `done` special packet. We don't send this on error,
log::error!(target: "worker", "Fetch error: {err}"); // as the remote will not be expecting it.
}
if let Err(err) = &result { if let Err(err) = &result {
log::error!(target: "worker", "Fetch error: {err}"); log::error!(target: "worker", "Fetch error: {err}");
} else if let Err(err) = pktline::done(&mut session) {
log::error!(target: "worker", "Fetch error: {err}");
} }
(session, result) (session, result)
} else { } else {
@ -237,12 +233,7 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
stream_r: &mut WireReader, stream_r: &mut WireReader,
stream_w: &mut WireWriter<G>, stream_w: &mut WireWriter<G>,
) -> Result<Vec<RefUpdate>, FetchError> { ) -> Result<Vec<RefUpdate>, FetchError> {
// Connect to our local git daemon, running as a child process.
let daemon = net::TcpStream::connect_timeout(&self.daemon, self.timeout)?;
let (mut daemon_r, mut daemon_w) = (daemon.try_clone()?, daemon);
let mut stream_r = pktline::Reader::new(drain, stream_r); let mut stream_r = pktline::Reader::new(drain, stream_r);
let mut daemon_r = pktline::Reader::new(vec![], &mut daemon_r);
let mut buffer = [0; u16::MAX as usize + 1];
// Read the request packet line to make sure the repository being requested matches what // Read the request packet line to make sure the repository being requested matches what
// we expect, and that the service requested is valid. // we expect, and that the service requested is valid.
@ -265,11 +256,19 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
)))); ))));
} }
}; };
// Connect to our local git daemon, running as a child process.
let daemon = net::TcpStream::connect_timeout(&self.daemon, self.timeout)?;
let (mut daemon_r, mut daemon_w) = (daemon.try_clone()?, daemon);
let mut daemon_r = pktline::Reader::new(vec![], &mut daemon_r);
// Write the raw request to the daemon, once we've verified it. // Write the raw request to the daemon, once we've verified it.
daemon_w.write_all(&request)?; daemon_w.write_all(&request)?;
// We now loop, alternating between reading requests from the client, and writing responses // We now loop, alternating between reading requests from the client, and writing responses
// back from the daemon.. Requests are delimited with a flush packet (`flush-pkt`). // back from the daemon.. Requests are delimited with a flush packet (`flush-pkt`).
let mut buffer = [0; u16::MAX as usize + 1];
loop { loop {
if let Err(e) = daemon_r.read_pktlines(stream_w, &mut buffer) { if let Err(e) = daemon_r.read_pktlines(stream_w, &mut buffer) {
// This is the expected error when the remote disconnects. // This is the expected error when the remote disconnects.