node: Implement protocol multiplexing
To improve the reliability and flexibility of the protocol, we introduce multiplexing over peer connections. This involves a new `Frame` type that carries a stream-id and payload. Three stream types are made available: 1. Control 2. Gossip 3. Git This change brings the following improvements: * Removed need to queue fetch requests * Removed need to queue gossip messages * Removed need for `Fetch` and `FetchOk` messages * Service doesn't need to know about inbound fetches * Removed one round-trip for fetch negotiation * Removed special `done` git packet * Removed session logic and state around Git/Fetch protocols * Removed code around upgrading/downgrading transport * Worker in responder mode is able to process any number of fetches * Connections support any number of concurrent fetches We had to introduce a few extra things however to make it all work: * A `VarInt` type for variable-length integers, since we want the frames to be lightweight. * A custom "tunnel" implementation, since we couldn't use the existing one anymore. Overall the change removes more complexity than it adds, while improving the protocol along the way.
This commit is contained in:
parent
0f47dc9057
commit
89b9eb53b7
|
|
@ -312,10 +312,12 @@ fn rad_clone() {
|
||||||
let working = environment.tmp().join("working");
|
let working = environment.tmp().join("working");
|
||||||
|
|
||||||
// Setup a test project.
|
// Setup a test project.
|
||||||
let _ = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
|
let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
|
||||||
|
|
||||||
let alice = alice.spawn(Config::default());
|
let mut alice = alice.spawn(Config::default());
|
||||||
let mut bob = bob.spawn(Config::default());
|
let mut bob = bob.spawn(Config::default());
|
||||||
|
// Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
|
||||||
|
alice.handle.track_repo(acme, Scope::Trusted).unwrap();
|
||||||
|
|
||||||
bob.connect(&alice).converge([&alice]);
|
bob.connect(&alice).converge([&alice]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct Deserializer<D = Message> {
|
||||||
item: PhantomData<D>,
|
item: PhantomData<D>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Deserializer<Message> {
|
impl<D: wire::Decode> Default for Deserializer<D> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(wire::Size::MAX as usize + 1)
|
Self::new(wire::Size::MAX as usize + 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,24 +96,24 @@ impl<T: Clone> Emitter<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Holds join handles to the client threads, as well as a client handle.
|
/// Holds join handles to the client threads, as well as a client handle.
|
||||||
pub struct Runtime<G: Signer + Ecdh> {
|
pub struct Runtime {
|
||||||
pub id: NodeId,
|
pub id: NodeId,
|
||||||
pub home: Home,
|
pub home: Home,
|
||||||
pub control: UnixListener,
|
pub control: UnixListener,
|
||||||
pub handle: Handle<G>,
|
pub handle: Handle,
|
||||||
pub storage: Storage,
|
pub storage: Storage,
|
||||||
pub reactor: Reactor<wire::Control<G>>,
|
pub reactor: Reactor<wire::Control>,
|
||||||
pub daemon: net::SocketAddr,
|
pub daemon: net::SocketAddr,
|
||||||
pub pool: worker::Pool,
|
pub pool: worker::Pool,
|
||||||
pub local_addrs: Vec<net::SocketAddr>,
|
pub local_addrs: Vec<net::SocketAddr>,
|
||||||
pub signals: chan::Receiver<()>,
|
pub signals: chan::Receiver<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + Ecdh + 'static> Runtime<G> {
|
impl Runtime {
|
||||||
/// Initialize the runtime.
|
/// Initialize the runtime.
|
||||||
///
|
///
|
||||||
/// This function spawns threads.
|
/// This function spawns threads.
|
||||||
pub fn init(
|
pub fn init<G: Signer + Ecdh + 'static>(
|
||||||
home: Home,
|
home: Home,
|
||||||
config: service::Config,
|
config: service::Config,
|
||||||
listen: Vec<net::SocketAddr>,
|
listen: Vec<net::SocketAddr>,
|
||||||
|
|
@ -121,7 +121,7 @@ impl<G: Signer + Ecdh + 'static> Runtime<G> {
|
||||||
daemon: net::SocketAddr,
|
daemon: net::SocketAddr,
|
||||||
signals: chan::Receiver<()>,
|
signals: chan::Receiver<()>,
|
||||||
signer: G,
|
signer: G,
|
||||||
) -> Result<Runtime<G>, Error>
|
) -> Result<Runtime, Error>
|
||||||
where
|
where
|
||||||
G: Ecdh<Pk = NodeId> + Clone,
|
G: Ecdh<Pk = NodeId> + Clone,
|
||||||
{
|
{
|
||||||
|
|
@ -160,7 +160,7 @@ impl<G: Signer + Ecdh + 'static> Runtime<G> {
|
||||||
emitter.clone(),
|
emitter.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (worker_send, worker_recv) = chan::unbounded::<worker::Task<G>>();
|
let (worker_send, worker_recv) = chan::unbounded::<worker::Task>();
|
||||||
let mut wire = Wire::new(service, worker_send, signer, proxy, clock);
|
let mut wire = Wire::new(service, worker_send, signer, proxy, clock);
|
||||||
let mut local_addrs = Vec::new();
|
let mut local_addrs = Vec::new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,9 @@ use std::sync::Arc;
|
||||||
use std::{fmt, io, time};
|
use std::{fmt, io, time};
|
||||||
|
|
||||||
use crossbeam_channel as chan;
|
use crossbeam_channel as chan;
|
||||||
use cyphernet::Ecdh;
|
|
||||||
use radicle::node::Seeds;
|
use radicle::node::Seeds;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::crypto::Signer;
|
|
||||||
use crate::identity::Id;
|
use crate::identity::Id;
|
||||||
use crate::node::{Command, FetchResult};
|
use crate::node::{Command, FetchResult};
|
||||||
use crate::profile::Home;
|
use crate::profile::Home;
|
||||||
|
|
@ -20,6 +18,7 @@ use crate::service::Event;
|
||||||
use crate::service::{CommandError, QueryState};
|
use crate::service::{CommandError, QueryState};
|
||||||
use crate::service::{NodeId, Sessions};
|
use crate::service::{NodeId, Sessions};
|
||||||
use crate::wire;
|
use crate::wire;
|
||||||
|
use crate::wire::StreamId;
|
||||||
use crate::worker::TaskResult;
|
use crate::worker::TaskResult;
|
||||||
|
|
||||||
/// An error resulting from a handle method.
|
/// An error resulting from a handle method.
|
||||||
|
|
@ -27,7 +26,7 @@ use crate::worker::TaskResult;
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// The command channel is no longer connected.
|
/// The command channel is no longer connected.
|
||||||
#[error("command channel is not connected")]
|
#[error("command channel is not connected")]
|
||||||
NotConnected,
|
ChannelDisconnected,
|
||||||
/// The command returned an error.
|
/// The command returned an error.
|
||||||
#[error("command failed: {0}")]
|
#[error("command failed: {0}")]
|
||||||
Command(#[from] CommandError),
|
Command(#[from] CommandError),
|
||||||
|
|
@ -41,7 +40,7 @@ pub enum Error {
|
||||||
|
|
||||||
impl From<chan::RecvError> for Error {
|
impl From<chan::RecvError> for Error {
|
||||||
fn from(_: chan::RecvError) -> Self {
|
fn from(_: chan::RecvError) -> Self {
|
||||||
Self::NotConnected
|
Self::ChannelDisconnected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,20 +48,20 @@ impl From<chan::RecvTimeoutError> for Error {
|
||||||
fn from(err: chan::RecvTimeoutError) -> Self {
|
fn from(err: chan::RecvTimeoutError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
chan::RecvTimeoutError::Timeout => Self::Timeout,
|
chan::RecvTimeoutError::Timeout => Self::Timeout,
|
||||||
chan::RecvTimeoutError::Disconnected => Self::NotConnected,
|
chan::RecvTimeoutError::Disconnected => Self::ChannelDisconnected,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<chan::SendError<T>> for Error {
|
impl<T> From<chan::SendError<T>> for Error {
|
||||||
fn from(_: chan::SendError<T>) -> Self {
|
fn from(_: chan::SendError<T>) -> Self {
|
||||||
Self::NotConnected
|
Self::ChannelDisconnected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Handle<G: Signer + Ecdh> {
|
pub struct Handle {
|
||||||
pub(crate) home: Home,
|
pub(crate) home: Home,
|
||||||
pub(crate) controller: reactor::Controller<wire::Control<G>>,
|
pub(crate) controller: reactor::Controller<wire::Control>,
|
||||||
|
|
||||||
/// Whether a shutdown was initiated or not. Prevents attempting to shutdown twice.
|
/// Whether a shutdown was initiated or not. Prevents attempting to shutdown twice.
|
||||||
shutdown: Arc<AtomicBool>,
|
shutdown: Arc<AtomicBool>,
|
||||||
|
|
@ -117,20 +116,20 @@ impl Events {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + Ecdh> Handle<G> {
|
impl Handle {
|
||||||
/// Subscribe to events stream.
|
/// Subscribe to events stream.
|
||||||
pub fn events(&self) -> Events {
|
pub fn events(&self) -> Events {
|
||||||
Events(self.emitter.subscribe())
|
Events(self.emitter.subscribe())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + Ecdh> fmt::Debug for Handle<G> {
|
impl fmt::Debug for Handle {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("Handle").field("home", &self.home).finish()
|
f.debug_struct("Handle").field("home", &self.home).finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + Ecdh> Clone for Handle<G> {
|
impl Clone for Handle {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
home: self.home.clone(),
|
home: self.home.clone(),
|
||||||
|
|
@ -141,10 +140,10 @@ impl<G: Signer + Ecdh> Clone for Handle<G> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + Ecdh + 'static> Handle<G> {
|
impl Handle {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
home: Home,
|
home: Home,
|
||||||
controller: reactor::Controller<wire::Control<G>>,
|
controller: reactor::Controller<wire::Control>,
|
||||||
emitter: Emitter<Event>,
|
emitter: Emitter<Event>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -155,22 +154,20 @@ impl<G: Signer + Ecdh + 'static> Handle<G> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn worker_result(&mut self, resp: TaskResult<G>) -> Result<(), Error> {
|
pub fn worker_result(&mut self, result: TaskResult) -> Result<(), io::Error> {
|
||||||
match self.controller.cmd(wire::Control::Worker(resp)) {
|
self.controller.cmd(wire::Control::Worker(result))
|
||||||
Ok(()) => {}
|
|
||||||
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Err(Error::NotConnected),
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn command(&self, cmd: service::Command) -> Result<(), Error> {
|
pub fn flush(&mut self, remote: NodeId, stream: StreamId) -> Result<(), io::Error> {
|
||||||
self.controller.cmd(wire::Control::User(cmd))?;
|
self.controller.cmd(wire::Control::Flush { remote, stream })
|
||||||
Ok(())
|
}
|
||||||
|
|
||||||
|
fn command(&self, cmd: service::Command) -> Result<(), io::Error> {
|
||||||
|
self.controller.cmd(wire::Control::User(cmd))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + Ecdh + 'static> radicle::node::Handle for Handle<G> {
|
impl radicle::node::Handle for Handle {
|
||||||
type Sessions = Sessions;
|
type Sessions = Sessions;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
|
|
@ -222,10 +219,12 @@ impl<G: Signer + Ecdh + 'static> radicle::node::Handle for Handle<G> {
|
||||||
|
|
||||||
fn announce_refs(&mut self, id: Id) -> Result<(), Error> {
|
fn announce_refs(&mut self, id: Id) -> Result<(), Error> {
|
||||||
self.command(service::Command::AnnounceRefs(id))
|
self.command(service::Command::AnnounceRefs(id))
|
||||||
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announce_inventory(&mut self) -> Result<(), Error> {
|
fn announce_inventory(&mut self) -> Result<(), Error> {
|
||||||
self.command(service::Command::AnnounceInventory)
|
self.command(service::Command::AnnounceInventory)
|
||||||
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_inventory(&mut self) -> Result<bool, Error> {
|
fn sync_inventory(&mut self) -> Result<bool, Error> {
|
||||||
|
|
@ -265,6 +264,8 @@ impl<G: Signer + Ecdh + 'static> radicle::node::Handle for Handle<G> {
|
||||||
.and_then(|sock| Command::SHUTDOWN.to_writer(sock))
|
.and_then(|sock| Command::SHUTDOWN.to_writer(sock))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
self.controller.shutdown().map_err(|_| Error::NotConnected)
|
self.controller
|
||||||
|
.shutdown()
|
||||||
|
.map_err(|_| Error::ChannelDisconnected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,6 @@ use crate::prelude::*;
|
||||||
use crate::runtime::Emitter;
|
use crate::runtime::Emitter;
|
||||||
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};
|
||||||
use crate::service::reactor::FetchDirection;
|
|
||||||
use crate::service::session::GossipState;
|
|
||||||
use crate::service::tracking::Scope;
|
use crate::service::tracking::Scope;
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
use crate::storage::{Namespaces, ReadStorage};
|
use crate::storage::{Namespaces, ReadStorage};
|
||||||
|
|
@ -44,7 +42,6 @@ use crate::Link;
|
||||||
pub use crate::node::NodeId;
|
pub use crate::node::NodeId;
|
||||||
pub use crate::service::config::{Config, Network};
|
pub use crate::service::config::{Config, Network};
|
||||||
pub use crate::service::message::{Message, ZeroBytes};
|
pub use crate::service::message::{Message, ZeroBytes};
|
||||||
pub use crate::service::reactor::Fetch;
|
|
||||||
pub use crate::service::session::Session;
|
pub use crate::service::session::Session;
|
||||||
|
|
||||||
use self::gossip::Gossip;
|
use self::gossip::Gossip;
|
||||||
|
|
@ -551,13 +548,12 @@ where
|
||||||
let seed = session.id;
|
let seed = session.id;
|
||||||
|
|
||||||
match session.fetch(rid) {
|
match session.fetch(rid) {
|
||||||
session::FetchResult::Ready(fetch) => {
|
session::FetchResult::Ready => {
|
||||||
debug!(target: "service", "Fetch initiated for {rid} with {seed}..");
|
debug!(target: "service", "Fetch initiated for {rid} with {seed}..");
|
||||||
|
|
||||||
match self.tracking.namespaces_for(&self.storage, &rid) {
|
match self.tracking.namespaces_for(&self.storage, &rid) {
|
||||||
Ok(ns) => {
|
Ok(namespaces) => {
|
||||||
self.reactor.write(session, fetch);
|
self.reactor.fetch(session, rid, namespaces);
|
||||||
session.to_requesting(rid, ns);
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(target: "service", "Error getting namespaces for {rid}: {err}");
|
error!(target: "service", "Error getting namespaces for {rid}: {err}");
|
||||||
|
|
@ -571,19 +567,8 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
session::FetchResult::AlreadyFetching(other) => {
|
session::FetchResult::AlreadyFetching => {
|
||||||
if other == rid {
|
|
||||||
debug!(target: "service", "Ignoring redundant attempt to fetch {rid} from {from}");
|
debug!(target: "service", "Ignoring redundant attempt to fetch {rid} from {from}");
|
||||||
} else {
|
|
||||||
// If we can't fetch, it's because we're already fetching from
|
|
||||||
// this peer. So we need to queue the request.
|
|
||||||
// TODO: consider to find another peer.
|
|
||||||
debug!(
|
|
||||||
target: "service",
|
|
||||||
"Queueing fetch for {rid} from {from}: another fetch is ongoing"
|
|
||||||
);
|
|
||||||
session.queue_fetch(rid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
session::FetchResult::NotConnected => {
|
session::FetchResult::NotConnected => {
|
||||||
error!(target: "service", "Unable to fetch {rid} from peer {seed}: peer is not connected");
|
error!(target: "service", "Unable to fetch {rid} from peer {seed}: peer is not connected");
|
||||||
|
|
@ -591,12 +576,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetched(&mut self, fetch: Fetch, result: Result<Vec<RefUpdate>, FetchError>) {
|
pub fn fetched(
|
||||||
let remote = fetch.remote;
|
&mut self,
|
||||||
let rid = fetch.rid;
|
rid: Id,
|
||||||
|
namespaces: Namespaces,
|
||||||
match fetch.direction {
|
remote: NodeId,
|
||||||
FetchDirection::Initiator { namespaces } => {
|
result: Result<Vec<RefUpdate>, FetchError>,
|
||||||
|
) {
|
||||||
let result = match result {
|
let result = match result {
|
||||||
Ok(updated) => {
|
Ok(updated) => {
|
||||||
log::debug!(target: "service", "Fetched {rid} from {remote}");
|
log::debug!(target: "service", "Fetched {rid} from {remote}");
|
||||||
|
|
@ -642,8 +628,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.switch_to_gossip(remote);
|
// Go back to "idle".
|
||||||
|
if let Some(s) = self.sessions.get_mut(&remote) {
|
||||||
|
s.fetched(rid);
|
||||||
|
}
|
||||||
// TODO: Since this fetch could be either a full clone
|
// TODO: Since this fetch could be either a full clone
|
||||||
// or simply a ref update, we need to either announce
|
// or simply a ref update, we need to either announce
|
||||||
// new inventory, or new refs. Right now, we announce
|
// new inventory, or new refs. Right now, we announce
|
||||||
|
|
@ -656,10 +644,6 @@ where
|
||||||
// to the gossip protocol, otherwise the messages will
|
// to the gossip protocol, otherwise the messages will
|
||||||
// be queued.
|
// be queued.
|
||||||
self.sync_and_announce();
|
self.sync_and_announce();
|
||||||
self.process_fetch_queue(&remote);
|
|
||||||
}
|
|
||||||
FetchDirection::Responder => self.switch_to_gossip(remote),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accepted(&mut self, _addr: net::SocketAddr) {
|
pub fn accepted(&mut self, _addr: net::SocketAddr) {
|
||||||
|
|
@ -725,8 +709,8 @@ where
|
||||||
|
|
||||||
// If the peer disconnected while we were waiting for a [`Message::FetchOk`],
|
// If the peer disconnected while we were waiting for a [`Message::FetchOk`],
|
||||||
// return a failure to any potential fetcher.
|
// return a failure to any potential fetcher.
|
||||||
if let Some((requested, _)) = session.requesting() {
|
for rid in session.fetching() {
|
||||||
if let Some(resp) = self.fetch_reqs.remove(&(*requested, remote)) {
|
if let Some(resp) = self.fetch_reqs.remove(&(rid, remote)) {
|
||||||
resp.send(FetchResult::Failed {
|
resp.send(FetchResult::Failed {
|
||||||
reason: format!("disconnected: {reason}"),
|
reason: format!("disconnected: {reason}"),
|
||||||
})
|
})
|
||||||
|
|
@ -902,9 +886,7 @@ where
|
||||||
if self.sessions.is_connected(announcer) {
|
if self.sessions.is_connected(announcer) {
|
||||||
match self.should_fetch_refs_announcement(message, &repo_entry.scope) {
|
match self.should_fetch_refs_announcement(message, &repo_entry.scope) {
|
||||||
Ok(true) => self.fetch(message.rid, announcer),
|
Ok(true) => self.fetch(message.rid, announcer),
|
||||||
Ok(false) => {
|
Ok(false) => {}
|
||||||
debug!(target: "service", "Skipping fetch from {announcer}")
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(target: "service", "Failed to check refs announcement: {e}");
|
error!(target: "service", "Failed to check refs announcement: {e}");
|
||||||
return Err(session::Error::Misbehavior);
|
return Err(session::Error::Misbehavior);
|
||||||
|
|
@ -1042,19 +1024,6 @@ where
|
||||||
debug!(target: "service", "Received message {:?} from {}", &message, peer.id);
|
debug!(target: "service", "Received message {:?} from {}", &message, peer.id);
|
||||||
|
|
||||||
match (&mut peer.state, message) {
|
match (&mut peer.state, message) {
|
||||||
(
|
|
||||||
session::State::Connected {
|
|
||||||
protocol: session::Protocol::Fetch { .. },
|
|
||||||
..
|
|
||||||
},
|
|
||||||
_,
|
|
||||||
) => {
|
|
||||||
// This should never happen if the service is properly configured, since all
|
|
||||||
// incoming data is sent directly to the Git worker.
|
|
||||||
log::error!(target: "service", "Received gossip message from {remote} during git fetch");
|
|
||||||
|
|
||||||
return Err(session::Error::Misbehavior);
|
|
||||||
}
|
|
||||||
// Process a peer announcement.
|
// Process a peer announcement.
|
||||||
(session::State::Connected { .. }, Message::Announcement(ann)) => {
|
(session::State::Connected { .. }, Message::Announcement(ann)) => {
|
||||||
let relayer = peer.id;
|
let relayer = peer.id;
|
||||||
|
|
@ -1107,72 +1076,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(
|
|
||||||
session::State::Connected {
|
|
||||||
protocol: session::Protocol::Gossip { state },
|
|
||||||
..
|
|
||||||
},
|
|
||||||
Message::Fetch { rid },
|
|
||||||
) => {
|
|
||||||
debug!(target: "service", "Fetch requested for {rid} from {remote}..");
|
|
||||||
|
|
||||||
// TODO: Check that we have the repo first?
|
|
||||||
|
|
||||||
// We got a fetch request right after sending our own. We have to decide on which
|
|
||||||
// fetch to run: our own, or the remote's.
|
|
||||||
if let GossipState::Requesting { rid, .. } = state {
|
|
||||||
debug!(target: "service", "Received fetch request from {remote} while attempting to fetch {rid}..");
|
|
||||||
|
|
||||||
// When fetch requests cross, the inbound peer takes precedence.
|
|
||||||
if peer.link.is_inbound() {
|
|
||||||
debug!(target: "service", "Cancelling fetch request to {remote}..");
|
|
||||||
|
|
||||||
// Cancel our own fetch request. This doesn't send anything to the remote,
|
|
||||||
// it simply updates the local session's state machine.
|
|
||||||
*state = GossipState::Idle;
|
|
||||||
|
|
||||||
// TODO: Queue the fetch request as if we tried to request twice from
|
|
||||||
// the same node.
|
|
||||||
} else {
|
|
||||||
// In this case, the remote node will cancel its request, so we don't
|
|
||||||
// want to handover the session to the worker here, we will do it when
|
|
||||||
// we get the `FetchOk` from the remote.
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept the request and instruct the transport to handover the socket to the worker.
|
|
||||||
self.reactor.write(peer, Message::FetchOk { rid });
|
|
||||||
self.reactor.fetch(peer, rid, FetchDirection::Responder);
|
|
||||||
}
|
|
||||||
(session::State::Connected { protocol, .. }, Message::FetchOk { rid }) => {
|
|
||||||
let session::Protocol::Gossip {
|
|
||||||
state: GossipState::Requesting { rid: requested, namespaces }
|
|
||||||
} = protocol else {
|
|
||||||
// As long as we disconnect peers who don't respond to our fetch requests within
|
|
||||||
// the alloted time, this shouldn't happen by mistake.
|
|
||||||
error!(
|
|
||||||
"Received unexpected message `fetch-ok` from peer {}",
|
|
||||||
peer.id
|
|
||||||
);
|
|
||||||
return Err(session::Error::Misbehavior);
|
|
||||||
};
|
|
||||||
|
|
||||||
if *requested != rid {
|
|
||||||
error!(
|
|
||||||
"Received `fetch-ok` from {} for incorrect repository {rid}",
|
|
||||||
peer.id
|
|
||||||
);
|
|
||||||
return Err(session::Error::Misbehavior);
|
|
||||||
}
|
|
||||||
let namespaces = namespaces.clone();
|
|
||||||
|
|
||||||
debug!(target: "service", "Fetch accepted for {rid} from {remote}..");
|
|
||||||
|
|
||||||
// Instruct the transport to handover the socket to the worker.
|
|
||||||
self.reactor
|
|
||||||
.fetch(peer, rid, FetchDirection::Initiator { namespaces });
|
|
||||||
}
|
|
||||||
(session::State::Attempted { .. } | session::State::Initial, msg) => {
|
(session::State::Attempted { .. } | session::State::Initial, msg) => {
|
||||||
error!(target: "service", "Received {:?} from connecting peer {}", msg, peer.id);
|
error!(target: "service", "Received {:?} from connecting peer {}", msg, peer.id);
|
||||||
}
|
}
|
||||||
|
|
@ -1299,19 +1202,6 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_to_gossip(&mut self, remote: NodeId) {
|
|
||||||
if let Some(session) = self.sessions.get_mut(&remote) {
|
|
||||||
// Transition session back to gossip protocol.
|
|
||||||
session.to_gossip();
|
|
||||||
// Drain any messages in the session's outbox, which might
|
|
||||||
// have accumulated during a fetch, and send them to the
|
|
||||||
// peer.
|
|
||||||
self.reactor.drain(session);
|
|
||||||
} else {
|
|
||||||
log::debug!(target: "service", "Session not found for {remote}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_and_announce(&mut self) {
|
fn sync_and_announce(&mut self) {
|
||||||
match self.sync_inventory() {
|
match self.sync_inventory() {
|
||||||
Ok(updated) => {
|
Ok(updated) => {
|
||||||
|
|
@ -1331,16 +1221,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the next pending fetch with `remote`, if any.
|
|
||||||
fn process_fetch_queue(&mut self, remote: &NodeId) {
|
|
||||||
if let Some(session) = self.sessions.get_mut(remote) {
|
|
||||||
if let Some(rid) = session.dequeue_fetch() {
|
|
||||||
debug!(target: "service", "Dequeued a pending fetch {rid} with {remote}");
|
|
||||||
self.fetch(rid, remote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reconnect(&mut self, nid: NodeId, addr: Address) -> bool {
|
fn reconnect(&mut self, nid: NodeId, addr: Address) -> bool {
|
||||||
if let Some(sess) = self.sessions.get_mut(&nid) {
|
if let Some(sess) = self.sessions.get_mut(&nid) {
|
||||||
sess.to_initial();
|
sess.to_initial();
|
||||||
|
|
@ -1370,7 +1250,6 @@ where
|
||||||
pub struct Stats {
|
pub struct Stats {
|
||||||
connected: usize,
|
connected: usize,
|
||||||
disconnected: usize,
|
disconnected: usize,
|
||||||
fetching: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (stats, seeds) = match self.routing.get(rid) {
|
let (stats, seeds) = match self.routing.get(rid) {
|
||||||
|
|
@ -1378,10 +1257,7 @@ where
|
||||||
(Stats::default(), Seeds::default()),
|
(Stats::default(), Seeds::default()),
|
||||||
|(mut stats, mut seeds), node| {
|
|(mut stats, mut seeds), node| {
|
||||||
if node != self.node_id() {
|
if node != self.node_id() {
|
||||||
if self.sessions.is_fetching(&node) {
|
if self.sessions.is_connected(&node) {
|
||||||
seeds.insert(Seed::Fetching(node));
|
|
||||||
stats.fetching += 1;
|
|
||||||
} else if self.sessions.is_connected(&node) {
|
|
||||||
seeds.insert(Seed::Connected(node));
|
seeds.insert(Seed::Connected(node));
|
||||||
stats.connected += 1;
|
stats.connected += 1;
|
||||||
} else if self.sessions.is_disconnected(&node) {
|
} else if self.sessions.is_disconnected(&node) {
|
||||||
|
|
@ -1399,8 +1275,8 @@ where
|
||||||
};
|
};
|
||||||
debug!(
|
debug!(
|
||||||
target: "service",
|
target: "service",
|
||||||
"Found {} connected seed(s), {} disconnected seed(s), and {} fetching seed(s) for {}",
|
"Found {} connected seed(s) and {} disconnected seed(s) for {}",
|
||||||
stats.connected, stats.disconnected, stats.fetching, rid
|
stats.connected, stats.disconnected, rid
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(seeds)
|
Ok(seeds)
|
||||||
|
|
@ -1788,10 +1664,6 @@ impl Sessions {
|
||||||
self.0.get(id).map(|s| s.is_connected()).unwrap_or(false)
|
self.0.get(id).map(|s| s.is_connected()).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_fetching(&self, id: &NodeId) -> bool {
|
|
||||||
self.0.get(id).map(|s| s.is_fetching()).unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return whether this node can be connected to.
|
/// Return whether this node can be connected to.
|
||||||
pub fn is_disconnected(&self, id: &NodeId) -> bool {
|
pub fn is_disconnected(&self, id: &NodeId) -> bool {
|
||||||
self.0.get(id).map(|s| s.is_disconnected()).unwrap_or(true)
|
self.0.get(id).map(|s| s.is_disconnected()).unwrap_or(true)
|
||||||
|
|
|
||||||
|
|
@ -341,12 +341,6 @@ pub enum Message {
|
||||||
/// The pong payload.
|
/// The pong payload.
|
||||||
zeroes: ZeroBytes,
|
zeroes: ZeroBytes,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Request a session upgrade to the Git protocol and fetch the given repository.
|
|
||||||
Fetch { rid: Id },
|
|
||||||
|
|
||||||
/// Accept a fetch request.
|
|
||||||
FetchOk { rid: Id },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Message {
|
impl PartialOrd for Message {
|
||||||
|
|
@ -442,8 +436,6 @@ impl fmt::Debug for Message {
|
||||||
}
|
}
|
||||||
Self::Ping(Ping { ponglen, zeroes }) => write!(f, "Ping({ponglen}, {zeroes:?})"),
|
Self::Ping(Ping { ponglen, zeroes }) => write!(f, "Ping({ponglen}, {zeroes:?})"),
|
||||||
Self::Pong { zeroes } => write!(f, "Pong({zeroes:?})"),
|
Self::Pong { zeroes } => write!(f, "Pong({zeroes:?})"),
|
||||||
Self::Fetch { rid } => write!(f, "Fetch({rid})"),
|
|
||||||
Self::FetchOk { rid } => write!(f, "FetchOk({rid})"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::VecDeque;
|
||||||
use std::mem;
|
|
||||||
|
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
||||||
|
|
@ -19,52 +18,16 @@ pub enum Io {
|
||||||
/// Disconnect from a peer.
|
/// Disconnect from a peer.
|
||||||
Disconnect(NodeId, DisconnectReason),
|
Disconnect(NodeId, DisconnectReason),
|
||||||
/// Fetch repository data from a peer.
|
/// Fetch repository data from a peer.
|
||||||
Fetch(Fetch),
|
Fetch {
|
||||||
/// Ask for a wakeup in a specified amount of time.
|
/// Repo being fetched.
|
||||||
Wakeup(LocalDuration),
|
rid: Id,
|
||||||
}
|
/// Remote node being fetched from.
|
||||||
|
remote: NodeId,
|
||||||
/// Fetch job sent to worker thread.
|
/// Namespaces being fetched.
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Fetch {
|
|
||||||
/// Repo to fetch.
|
|
||||||
pub rid: Id,
|
|
||||||
/// Indicates whether the fetch request was initiated or is a response.
|
|
||||||
pub direction: FetchDirection,
|
|
||||||
/// Remote peer we are interacting with.
|
|
||||||
pub remote: NodeId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Fetch {
|
|
||||||
pub fn is_initiator(&self) -> bool {
|
|
||||||
self.direction.is_initiator()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initiated(&self) -> Option<&Namespaces> {
|
|
||||||
match &self.direction {
|
|
||||||
FetchDirection::Initiator { namespaces } => Some(namespaces),
|
|
||||||
FetchDirection::Responder => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum FetchDirection {
|
|
||||||
/// Client is initiating a fetch in order to receive the specified
|
|
||||||
/// `refspecs` determined by [`Namespaces`].
|
|
||||||
Initiator {
|
|
||||||
/// Namespaces to fetch.
|
|
||||||
namespaces: Namespaces,
|
namespaces: Namespaces,
|
||||||
},
|
},
|
||||||
/// Server is responding to a fetch request by uploading the
|
/// Ask for a wakeup in a specified amount of time.
|
||||||
/// specified `refspecs` sent by the client.
|
Wakeup(LocalDuration),
|
||||||
Responder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FetchDirection {
|
|
||||||
pub fn is_initiator(&self) -> bool {
|
|
||||||
matches!(self, Self::Initiator { .. })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interface to the network reactor.
|
/// Interface to the network reactor.
|
||||||
|
|
@ -72,10 +35,6 @@ impl FetchDirection {
|
||||||
pub struct Reactor {
|
pub struct Reactor {
|
||||||
/// Outgoing I/O queue.
|
/// Outgoing I/O queue.
|
||||||
io: VecDeque<Io>,
|
io: VecDeque<Io>,
|
||||||
/// Message outbox for each node.
|
|
||||||
/// If messages can't be sent to a node immediately, they are stored in the outbox.
|
|
||||||
/// This can happen if for eg. a fetch is ongoing with that node.
|
|
||||||
outbox: HashMap<NodeId, Vec<Message>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reactor {
|
impl Reactor {
|
||||||
|
|
@ -90,32 +49,14 @@ impl Reactor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&mut self, remote: &Session, msg: Message) {
|
pub fn write(&mut self, remote: &Session, msg: Message) {
|
||||||
// If we've requested a fetch or are currently fetching, any message to be written
|
|
||||||
// to the remote peer should be queued.
|
|
||||||
if remote.is_requesting() || remote.is_fetching() {
|
|
||||||
debug!(target: "service", "Queue {:?} for {}", &msg, remote);
|
|
||||||
self.outbox.entry(remote.id).or_default().push(msg);
|
|
||||||
} else {
|
|
||||||
debug!(target: "service", "Write {:?} to {}", &msg, remote);
|
debug!(target: "service", "Write {:?} to {}", &msg, remote);
|
||||||
self.io.push_back(Io::Write(remote.id, vec![msg]));
|
self.io.push_back(Io::Write(remote.id, vec![msg]));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_all(&mut self, remote: &Session, msgs: impl IntoIterator<Item = Message>) {
|
pub fn write_all(&mut self, remote: &Session, msgs: impl IntoIterator<Item = Message>) {
|
||||||
let msgs = msgs.into_iter().collect::<Vec<_>>();
|
let msgs = msgs.into_iter().collect::<Vec<_>>();
|
||||||
let queue = remote.is_fetching() || remote.is_requesting();
|
|
||||||
|
|
||||||
for (ix, msg) in msgs.iter().enumerate() {
|
for (ix, msg) in msgs.iter().enumerate() {
|
||||||
if queue {
|
|
||||||
debug!(
|
|
||||||
target: "service",
|
|
||||||
"Queue {:?} for {} ({}/{})",
|
|
||||||
msg,
|
|
||||||
remote,
|
|
||||||
ix + 1,
|
|
||||||
msgs.len()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
debug!(
|
debug!(
|
||||||
target: "service",
|
target: "service",
|
||||||
"Write {:?} to {} ({}/{})",
|
"Write {:?} to {} ({}/{})",
|
||||||
|
|
@ -125,36 +66,19 @@ impl Reactor {
|
||||||
msgs.len()
|
msgs.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if queue {
|
|
||||||
self.outbox.entry(remote.id).or_default().extend(msgs);
|
|
||||||
} else {
|
|
||||||
self.io.push_back(Io::Write(remote.id, msgs));
|
self.io.push_back(Io::Write(remote.id, msgs));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn drain(&mut self, remote: &Session) {
|
|
||||||
if let Some(outbox) = self.outbox.get_mut(&remote.id) {
|
|
||||||
debug!(target: "service", "Draining outbox for session {} ({} message(s))", remote.id, outbox.len());
|
|
||||||
|
|
||||||
let msgs = mem::take(outbox);
|
|
||||||
self.write_all(remote, msgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wakeup(&mut self, after: LocalDuration) {
|
pub fn wakeup(&mut self, after: LocalDuration) {
|
||||||
self.io.push_back(Io::Wakeup(after));
|
self.io.push_back(Io::Wakeup(after));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(&mut self, remote: &mut Session, rid: Id, direction: FetchDirection) {
|
pub fn fetch(&mut self, remote: &mut Session, rid: Id, namespaces: Namespaces) {
|
||||||
// Transition the session state machine to "fetching".
|
self.io.push_back(Io::Fetch {
|
||||||
remote.to_fetching(rid);
|
|
||||||
|
|
||||||
self.io.push_back(Io::Fetch(Fetch {
|
|
||||||
rid,
|
rid,
|
||||||
direction,
|
namespaces,
|
||||||
remote: remote.id,
|
remote: remote.id,
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Broadcast a message to a list of peers.
|
/// Broadcast a message to a list of peers.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use std::collections::VecDeque;
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use radicle::storage::Namespaces;
|
|
||||||
|
|
||||||
use crate::service::message;
|
use crate::service::message;
|
||||||
use crate::service::message::Message;
|
use crate::service::message::Message;
|
||||||
use crate::service::{Id, LocalTime, NodeId, Reactor, Rng};
|
use crate::service::{Id, LocalTime, NodeId, Reactor, Rng};
|
||||||
|
|
@ -19,35 +17,6 @@ pub enum PingState {
|
||||||
Ok,
|
Ok,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sub-state of the gossip protocol.
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
|
||||||
pub enum GossipState {
|
|
||||||
/// Regular gossip, no pending fetch requests.
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
/// Requesting a fetch for the given RID. Waiting for a [`Message::FetchOk`].
|
|
||||||
Requesting { rid: Id, namespaces: Namespaces },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Session protocol.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum Protocol {
|
|
||||||
/// The default message-based gossip protocol.
|
|
||||||
Gossip { state: GossipState },
|
|
||||||
/// Git smart protocol. Used for fetching repository data.
|
|
||||||
/// This protocol is used after a connection upgrade via the
|
|
||||||
/// [`Message::Fetch`] message.
|
|
||||||
Fetch { rid: Id },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Protocol {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Gossip {
|
|
||||||
state: GossipState::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
|
|
@ -61,8 +30,8 @@ pub enum State {
|
||||||
since: LocalTime,
|
since: LocalTime,
|
||||||
/// Ping state.
|
/// Ping state.
|
||||||
ping: PingState,
|
ping: PingState,
|
||||||
/// Session protocol.
|
/// Ongoing fetches.
|
||||||
protocol: Protocol,
|
fetching: HashSet<Id>,
|
||||||
},
|
},
|
||||||
/// When a peer is disconnected.
|
/// When a peer is disconnected.
|
||||||
Disconnected {
|
Disconnected {
|
||||||
|
|
@ -82,23 +51,9 @@ impl fmt::Display for State {
|
||||||
Self::Attempted => {
|
Self::Attempted => {
|
||||||
write!(f, "attempted")
|
write!(f, "attempted")
|
||||||
}
|
}
|
||||||
Self::Connected { protocol, .. } => match protocol {
|
Self::Connected { .. } => {
|
||||||
Protocol::Gossip {
|
write!(f, "connected")
|
||||||
state: GossipState::Idle,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
write!(f, "connected <gossip>")
|
|
||||||
}
|
}
|
||||||
Protocol::Gossip {
|
|
||||||
state: GossipState::Requesting { rid, .. },
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
write!(f, "connected <gossip> requested={rid}")
|
|
||||||
}
|
|
||||||
Protocol::Fetch { .. } => {
|
|
||||||
write!(f, "connected <fetch>")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Self::Disconnected { .. } => {
|
Self::Disconnected { .. } => {
|
||||||
write!(f, "disconnected")
|
write!(f, "disconnected")
|
||||||
}
|
}
|
||||||
|
|
@ -109,10 +64,10 @@ impl fmt::Display for State {
|
||||||
/// Return value of [`Session::fetch`].
|
/// Return value of [`Session::fetch`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FetchResult {
|
pub enum FetchResult {
|
||||||
/// We are already fetching from this peer.
|
/// We are already fetching the given repo from this peer.
|
||||||
AlreadyFetching(Id),
|
AlreadyFetching,
|
||||||
/// Ok, ready to fetch.
|
/// Ok, ready to fetch.
|
||||||
Ready(Message),
|
Ready,
|
||||||
/// This peer is not ready to fetch.
|
/// This peer is not ready to fetch.
|
||||||
NotConnected,
|
NotConnected,
|
||||||
}
|
}
|
||||||
|
|
@ -164,9 +119,6 @@ pub struct Session {
|
||||||
/// 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,
|
||||||
|
|
||||||
/// Fetches queued due to another ongoing fetch.
|
|
||||||
pending_fetches: VecDeque<Id>,
|
|
||||||
|
|
||||||
/// 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
|
||||||
/// upon successful connection.
|
/// upon successful connection.
|
||||||
|
|
@ -205,7 +157,6 @@ impl Session {
|
||||||
persistent,
|
persistent,
|
||||||
last_active: LocalTime::default(),
|
last_active: LocalTime::default(),
|
||||||
attempts: 1,
|
attempts: 1,
|
||||||
pending_fetches: VecDeque::new(),
|
|
||||||
rng,
|
rng,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,14 +167,13 @@ impl Session {
|
||||||
state: State::Connected {
|
state: State::Connected {
|
||||||
since: time,
|
since: time,
|
||||||
ping: PingState::default(),
|
ping: PingState::default(),
|
||||||
protocol: Protocol::default(),
|
fetching: HashSet::default(),
|
||||||
},
|
},
|
||||||
link: Link::Inbound,
|
link: Link::Inbound,
|
||||||
subscribe: None,
|
subscribe: None,
|
||||||
persistent,
|
persistent,
|
||||||
last_active: LocalTime::default(),
|
last_active: LocalTime::default(),
|
||||||
attempts: 0,
|
attempts: 0,
|
||||||
pending_fetches: VecDeque::new(),
|
|
||||||
rng,
|
rng,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -236,16 +186,6 @@ impl Session {
|
||||||
matches!(self.state, State::Connected { .. })
|
matches!(self.state, State::Connected { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_fetching(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.state,
|
|
||||||
State::Connected {
|
|
||||||
protocol: Protocol::Fetch { .. },
|
|
||||||
..
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_disconnected(&self) -> bool {
|
pub fn is_disconnected(&self) -> bool {
|
||||||
matches!(self.state, State::Disconnected { .. })
|
matches!(self.state, State::Disconnected { .. })
|
||||||
}
|
}
|
||||||
|
|
@ -254,64 +194,26 @@ impl Session {
|
||||||
matches!(self.state, State::Initial)
|
matches!(self.state, State::Initial)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_requesting(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.state,
|
|
||||||
State::Connected {
|
|
||||||
protocol: Protocol::Gossip {
|
|
||||||
state: GossipState::Requesting { .. }
|
|
||||||
},
|
|
||||||
..
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attempts(&self) -> usize {
|
pub fn attempts(&self) -> usize {
|
||||||
self.attempts
|
self.attempts
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(&self, rid: Id) -> FetchResult {
|
pub fn fetch(&mut self, rid: Id) -> FetchResult {
|
||||||
if let State::Connected { protocol, .. } = &self.state {
|
if let State::Connected { fetching, .. } = &mut self.state {
|
||||||
match protocol {
|
if fetching.insert(rid) {
|
||||||
Protocol::Gossip { state } => {
|
FetchResult::Ready
|
||||||
if let GossipState::Requesting { rid, .. } = state {
|
|
||||||
FetchResult::AlreadyFetching(*rid)
|
|
||||||
} else {
|
} else {
|
||||||
FetchResult::Ready(Message::Fetch { rid })
|
FetchResult::AlreadyFetching
|
||||||
}
|
|
||||||
}
|
|
||||||
Protocol::Fetch { rid } => FetchResult::AlreadyFetching(*rid),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
FetchResult::NotConnected
|
FetchResult::NotConnected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_requesting(&mut self, rid: Id, namespaces: Namespaces) {
|
pub fn fetched(&mut self, rid: Id) {
|
||||||
let State::Connected { protocol, .. } = &mut self.state else {
|
if let State::Connected { fetching, .. } = &mut self.state {
|
||||||
panic!("Session::to_requesting: cannot transition to 'requesting': session is not connected");
|
if !fetching.remove(&rid) {
|
||||||
};
|
log::error!(target: "service", "Fetched unknown repository {rid}");
|
||||||
*protocol = Protocol::Gossip {
|
|
||||||
state: GossipState::Requesting { rid, namespaces },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_fetching(&mut self, rid: Id) {
|
|
||||||
let State::Connected { protocol, .. } = &mut self.state else {
|
|
||||||
panic!("Session::to_fetching: cannot transition to 'fetching': session is not connected");
|
|
||||||
};
|
|
||||||
*protocol = Protocol::Fetch { rid };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_gossip(&mut self) {
|
|
||||||
if let State::Connected { protocol, .. } = &mut self.state {
|
|
||||||
if let Protocol::Fetch { .. } = protocol {
|
|
||||||
*protocol = Protocol::default();
|
|
||||||
} else {
|
|
||||||
panic!(
|
|
||||||
"Unexpected session state for {}: expected 'fetch' protocol, got 'gossip'",
|
|
||||||
self.id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +236,7 @@ impl Session {
|
||||||
self.state = State::Connected {
|
self.state = State::Connected {
|
||||||
since,
|
since,
|
||||||
ping: PingState::default(),
|
ping: PingState::default(),
|
||||||
protocol: Protocol::default(),
|
fetching: HashSet::default(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,18 +256,11 @@ impl Session {
|
||||||
self.state = State::Initial;
|
self.state = State::Initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requesting(&self) -> Option<(&Id, &Namespaces)> {
|
pub fn fetching(&self) -> HashSet<Id> {
|
||||||
if let State::Connected {
|
if let State::Connected { fetching, .. } = &self.state {
|
||||||
protocol:
|
fetching.clone()
|
||||||
Protocol::Gossip {
|
|
||||||
state: GossipState::Requesting { rid, namespaces },
|
|
||||||
},
|
|
||||||
..
|
|
||||||
} = &self.state
|
|
||||||
{
|
|
||||||
Some((rid, namespaces))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
HashSet::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,12 +273,4 @@ impl Session {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn queue_fetch(&mut self, rid: Id) {
|
|
||||||
self.pending_fetches.push_back(rid);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn dequeue_fetch(&mut self) -> Option<Id> {
|
|
||||||
self.pending_fetches.pop_front()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,6 @@ impl Arbitrary for Message {
|
||||||
MessageType::Pong => Self::Pong {
|
MessageType::Pong => Self::Pong {
|
||||||
zeroes: ZeroBytes::new(u16::arbitrary(g).min(Ping::MAX_PONG_ZEROES)),
|
zeroes: ZeroBytes::new(u16::arbitrary(g).min(Ping::MAX_PONG_ZEROES)),
|
||||||
},
|
},
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ pub struct NodeHandle<G: Signer + cyphernet::Ecdh + 'static> {
|
||||||
pub home: Home,
|
pub home: Home,
|
||||||
pub addr: net::SocketAddr,
|
pub addr: net::SocketAddr,
|
||||||
pub thread: ManuallyDrop<thread::JoinHandle<Result<(), runtime::Error>>>,
|
pub thread: ManuallyDrop<thread::JoinHandle<Result<(), runtime::Error>>>,
|
||||||
pub handle: ManuallyDrop<Handle<G>>,
|
pub handle: ManuallyDrop<Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + cyphernet::Ecdh + 'static> Drop for NodeHandle<G> {
|
impl<G: Signer + cyphernet::Ecdh + 'static> Drop for NodeHandle<G> {
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ use log::*;
|
||||||
|
|
||||||
use crate::crypto::Signer;
|
use crate::crypto::Signer;
|
||||||
use crate::git::raw as git;
|
use crate::git::raw as git;
|
||||||
use crate::prelude::Address;
|
use crate::prelude::{Address, Id};
|
||||||
use crate::service::reactor::Io;
|
use crate::service::reactor::Io;
|
||||||
use crate::service::{DisconnectReason, Event, Fetch, Message, NodeId};
|
use crate::service::{DisconnectReason, Event, 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;
|
||||||
|
|
@ -63,7 +63,12 @@ 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(Fetch, Rc<Result<Vec<RefUpdate>, FetchError>>),
|
Fetched(
|
||||||
|
Id,
|
||||||
|
Namespaces,
|
||||||
|
NodeId,
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
|
|
@ -105,15 +110,8 @@ impl fmt::Display for Scheduled {
|
||||||
Input::Wake => {
|
Input::Wake => {
|
||||||
write!(f, "{}: Tock", self.node)
|
write!(f, "{}: Tock", self.node)
|
||||||
}
|
}
|
||||||
Input::Fetched(fetch, _) => {
|
Input::Fetched(rid, _, nid, _) => {
|
||||||
write!(
|
write!(f, "{} <<~ {} ({}): Fetched", self.node, nid, rid)
|
||||||
f,
|
|
||||||
"{} <<~ {} ({}): Fetched (initiated={})",
|
|
||||||
self.node,
|
|
||||||
fetch.remote,
|
|
||||||
fetch.rid,
|
|
||||||
fetch.is_initiator()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -411,17 +409,15 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
p.received_message(id, msg);
|
p.received_message(id, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Input::Fetched(f, result) => {
|
Input::Fetched(rid, ns, nid, result) => {
|
||||||
let result = Rc::try_unwrap(result).unwrap();
|
let result = Rc::try_unwrap(result).unwrap();
|
||||||
if let Some(namespaces) = f.initiated() {
|
let mut repo = match p.storage().repository_mut(rid) {
|
||||||
let mut repo = match p.storage().repository_mut(f.rid) {
|
|
||||||
Ok(repo) => repo,
|
Ok(repo) => repo,
|
||||||
Err(e) if e.is_not_found() => p.storage().create(f.rid).unwrap(),
|
Err(e) if e.is_not_found() => p.storage().create(rid).unwrap(),
|
||||||
Err(e) => panic!("Failed to open repository: {e}"),
|
Err(e) => panic!("Failed to open repository: {e}"),
|
||||||
};
|
};
|
||||||
fetch(&mut repo, &f.remote, namespaces.clone()).unwrap();
|
fetch(&mut repo, &nid, ns.clone()).unwrap();
|
||||||
}
|
p.fetched(rid, ns, nid, result);
|
||||||
p.fetched(f, result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while let Some(o) = p.next() {
|
while let Some(o) = p.next() {
|
||||||
|
|
@ -606,22 +602,16 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Io::Fetch(fetch) => {
|
Io::Fetch {
|
||||||
let remote = fetch.remote;
|
rid,
|
||||||
|
remote,
|
||||||
if fetch.is_initiator() {
|
namespaces,
|
||||||
|
} => {
|
||||||
log::info!(
|
log::info!(
|
||||||
target: "sim",
|
target: "sim",
|
||||||
"{:05} {} ~> {} ({}): Fetch outgoing",
|
"{:05} {} ~> {} ({}): Fetch outgoing",
|
||||||
self.elapsed().as_millis(), node, remote, fetch.rid
|
self.elapsed().as_millis(), node, remote, rid
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
log::info!(
|
|
||||||
target: "sim",
|
|
||||||
"{:05} {} <~ {} ({}): Fetch incoming",
|
|
||||||
self.elapsed().as_millis(), node, remote, fetch.rid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.is_fallible() {
|
if self.is_fallible() {
|
||||||
self.inbox.insert(
|
self.inbox.insert(
|
||||||
|
|
@ -630,7 +620,9 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
node,
|
node,
|
||||||
remote,
|
remote,
|
||||||
input: Input::Fetched(
|
input: Input::Fetched(
|
||||||
fetch,
|
rid,
|
||||||
|
namespaces,
|
||||||
|
remote,
|
||||||
Rc::new(Err(FetchError::Io(io::ErrorKind::Other.into()))),
|
Rc::new(Err(FetchError::Io(io::ErrorKind::Other.into()))),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -641,7 +633,7 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
Scheduled {
|
Scheduled {
|
||||||
node,
|
node,
|
||||||
remote,
|
remote,
|
||||||
input: Input::Fetched(fetch, Rc::new(Ok(vec![]))),
|
input: Input::Fetched(rid, namespaces, remote, Rc::new(Ok(vec![]))),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,12 @@ use crate::prelude::{LocalDuration, Timestamp};
|
||||||
use crate::service::config::*;
|
use crate::service::config::*;
|
||||||
use crate::service::filter::Filter;
|
use crate::service::filter::Filter;
|
||||||
use crate::service::message::*;
|
use crate::service::message::*;
|
||||||
use crate::service::reactor::FetchDirection;
|
|
||||||
use crate::service::reactor::Io;
|
use crate::service::reactor::Io;
|
||||||
use crate::service::ServiceState as _;
|
use crate::service::ServiceState as _;
|
||||||
use crate::service::*;
|
use crate::service::*;
|
||||||
use crate::storage::git::transport::{local, remote};
|
use crate::storage::git::transport::{local, remote};
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
use crate::storage::{Namespaces, ReadStorage};
|
use crate::storage::ReadStorage;
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
use crate::test::assert_matches;
|
use crate::test::assert_matches;
|
||||||
use crate::test::fixtures;
|
use crate::test::fixtures;
|
||||||
|
|
@ -707,10 +706,7 @@ fn test_refs_announcement_fetch_trusted_no_inventory() {
|
||||||
alice.receive(bob.id(), bob.refs_announcement(rid));
|
alice.receive(bob.id(), bob.refs_announcement(rid));
|
||||||
|
|
||||||
// Alice fetches Bob's refs as this is a new repo.
|
// Alice fetches Bob's refs as this is a new repo.
|
||||||
assert_eq!(
|
assert_matches!(alice.outbox().next(), Some(Io::Fetch { .. }));
|
||||||
alice.messages(bob.id()).next(),
|
|
||||||
Some(Message::Fetch { rid })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alice and Bob both have the same repo.
|
/// Alice and Bob both have the same repo.
|
||||||
|
|
@ -758,7 +754,7 @@ fn test_refs_announcement_trusted() {
|
||||||
// Bob announces refs again.
|
// Bob announces refs again.
|
||||||
bob.elapse(LocalDuration::from_mins(1)); // Make sure our announcement is fresh.
|
bob.elapse(LocalDuration::from_mins(1)); // Make sure our announcement is fresh.
|
||||||
alice.receive(bob.id(), bob.refs_announcement(rid));
|
alice.receive(bob.id(), bob.refs_announcement(rid));
|
||||||
assert_matches!(alice.messages(bob.id()).next(), Some(Message::Fetch { .. }));
|
assert_matches!(alice.outbox().next(), Some(Io::Fetch { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -778,86 +774,6 @@ fn test_refs_announcement_no_subscribe() {
|
||||||
assert!(alice.messages(eve.id()).next().is_none());
|
assert!(alice.messages(eve.id()).next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_gossip_during_fetch() {
|
|
||||||
let storage = arbitrary::nonempty_storage(1);
|
|
||||||
let rid = *storage.inventory.keys().next().unwrap();
|
|
||||||
let mut alice = Peer::with_storage("alice", [7, 7, 7, 7], storage);
|
|
||||||
let bob = Peer::new("bob", [8, 8, 8, 8]);
|
|
||||||
let eve = Peer::new("eve", [9, 9, 9, 9]);
|
|
||||||
let now = LocalTime::now().as_millis();
|
|
||||||
let (send, _recv) = chan::bounded::<node::FetchResult>(1);
|
|
||||||
let inventory1 = BoundedVec::try_from(arbitrary::vec(1)).unwrap();
|
|
||||||
let inventory2 = BoundedVec::try_from(arbitrary::vec(1)).unwrap();
|
|
||||||
|
|
||||||
alice.connect_to(&bob);
|
|
||||||
alice.connect_to(&eve);
|
|
||||||
alice.command(Command::Fetch(rid, bob.id, send));
|
|
||||||
|
|
||||||
assert_matches!(alice.messages(bob.id).next(), Some(Message::Fetch { .. }));
|
|
||||||
|
|
||||||
logger::init(log::Level::Debug);
|
|
||||||
|
|
||||||
alice.receive(
|
|
||||||
eve.id(),
|
|
||||||
Message::inventory(
|
|
||||||
InventoryAnnouncement {
|
|
||||||
inventory: inventory1.clone(),
|
|
||||||
timestamp: now + 1,
|
|
||||||
},
|
|
||||||
eve.signer(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// We shouldn't relay to Bob while we're fetching from him.
|
|
||||||
assert_matches!(alice.messages(bob.id).next(), None);
|
|
||||||
|
|
||||||
alice.receive(bob.id(), Message::FetchOk { rid });
|
|
||||||
alice.receive(
|
|
||||||
eve.id(),
|
|
||||||
Message::inventory(
|
|
||||||
InventoryAnnouncement {
|
|
||||||
inventory: inventory2.clone(),
|
|
||||||
timestamp: now + 2,
|
|
||||||
},
|
|
||||||
eve.signer(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// We shouldn't relay to Bob while we're fetching from him.
|
|
||||||
assert_matches!(alice.messages(bob.id).next(), None);
|
|
||||||
|
|
||||||
// Have enough time pass that Alice sends a "ping" to Bob.
|
|
||||||
alice.elapse(KEEP_ALIVE_DELTA);
|
|
||||||
|
|
||||||
// Now that the fetch is done, the messages Bob missed should be relayed to him.
|
|
||||||
alice.fetched(
|
|
||||||
Fetch {
|
|
||||||
rid,
|
|
||||||
direction: FetchDirection::Initiator {
|
|
||||||
namespaces: Namespaces::All,
|
|
||||||
},
|
|
||||||
remote: bob.id,
|
|
||||||
},
|
|
||||||
Ok(vec![]),
|
|
||||||
);
|
|
||||||
let mut messages = alice.messages(bob.id);
|
|
||||||
|
|
||||||
assert_matches!(
|
|
||||||
messages.next(),
|
|
||||||
Some(Message::Announcement(Announcement {
|
|
||||||
message: AnnouncementMessage::Inventory(InventoryAnnouncement { inventory, .. }),
|
|
||||||
..
|
|
||||||
})) if inventory == inventory1
|
|
||||||
);
|
|
||||||
assert_matches!(
|
|
||||||
messages.next(),
|
|
||||||
Some(Message::Announcement(Announcement {
|
|
||||||
message: AnnouncementMessage::Inventory(InventoryAnnouncement { inventory, .. }),
|
|
||||||
..
|
|
||||||
})) if inventory == inventory2
|
|
||||||
);
|
|
||||||
assert_matches!(messages.next(), Some(Message::Ping { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inventory_relay() {
|
fn test_inventory_relay() {
|
||||||
// Topology is eve <-> alice <-> bob
|
// Topology is eve <-> alice <-> bob
|
||||||
|
|
@ -1155,12 +1071,12 @@ fn test_fetch_missing_inventory() {
|
||||||
|
|
||||||
alice.elapse(service::SYNC_INTERVAL);
|
alice.elapse(service::SYNC_INTERVAL);
|
||||||
alice
|
alice
|
||||||
.messages(bob.id)
|
.outbox()
|
||||||
.find(|m| matches!(m, Message::Fetch { .. }))
|
.find(|m| matches!(m, Io::Fetch { .. }))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
alice
|
alice
|
||||||
.messages(eve.id)
|
.outbox()
|
||||||
.find(|m| matches!(m, Message::Fetch { .. }))
|
.find(|m| matches!(m, Io::Fetch { .. }))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1356,89 +1272,3 @@ fn prop_inventory_exchange_dense() {
|
||||||
.tests(20)
|
.tests(20)
|
||||||
.quickcheck(property as fn(MockStorage, MockStorage, MockStorage));
|
.quickcheck(property as fn(MockStorage, MockStorage, MockStorage));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_queued_fetch() {
|
|
||||||
let storage = arbitrary::nonempty_storage(3);
|
|
||||||
let mut repo_keys = storage.inventory.keys();
|
|
||||||
let rid = *repo_keys.next().unwrap();
|
|
||||||
let rid2 = *repo_keys.next().unwrap();
|
|
||||||
let rid3 = *repo_keys.next().unwrap();
|
|
||||||
let mut alice = Peer::with_storage("alice", [7, 7, 7, 7], storage);
|
|
||||||
let bob = Peer::new("bob", [8, 8, 8, 8]);
|
|
||||||
let (send, _recv) = chan::bounded::<node::FetchResult>(1);
|
|
||||||
|
|
||||||
logger::init(log::Level::Debug);
|
|
||||||
|
|
||||||
// Send the first fetch.
|
|
||||||
alice.connect_to(&bob);
|
|
||||||
alice.command(Command::Fetch(rid, bob.id, send));
|
|
||||||
|
|
||||||
assert_matches!(alice.messages(bob.id).next(), Some(Message::Fetch { .. }));
|
|
||||||
|
|
||||||
// Send the 2nd fetch that will be queued.
|
|
||||||
let (send2, _recv2) = chan::bounded::<node::FetchResult>(1);
|
|
||||||
alice.command(Command::Fetch(rid2, bob.id, send2));
|
|
||||||
|
|
||||||
// Send the 3rd fetch that will be queued.
|
|
||||||
let (send3, _recv3) = chan::bounded::<node::FetchResult>(1);
|
|
||||||
alice.command(Command::Fetch(rid3, bob.id, send3));
|
|
||||||
|
|
||||||
// We shouldn't send out the 2nd, 3rd fetch while we're doing the 1st fetch.
|
|
||||||
assert_matches!(alice.messages(bob.id).next(), None);
|
|
||||||
|
|
||||||
alice.receive(bob.id(), Message::FetchOk { rid });
|
|
||||||
assert_matches!(alice.messages(bob.id).next(), None);
|
|
||||||
|
|
||||||
// Have enough time pass that Alice sends a "ping" to Bob.
|
|
||||||
alice.elapse(KEEP_ALIVE_DELTA);
|
|
||||||
|
|
||||||
// Finish the 1st fetch.
|
|
||||||
alice.fetched(
|
|
||||||
Fetch {
|
|
||||||
rid,
|
|
||||||
direction: FetchDirection::Initiator {
|
|
||||||
namespaces: Namespaces::All,
|
|
||||||
},
|
|
||||||
remote: bob.id,
|
|
||||||
},
|
|
||||||
Ok(vec![]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Now the 1st fetch is done, the gossip messages are drained.
|
|
||||||
let mut messages = alice.messages(bob.id);
|
|
||||||
assert_matches!(messages.next(), Some(Message::Ping(_)));
|
|
||||||
|
|
||||||
// The message after all queued gossip messages is Fetch.
|
|
||||||
assert_eq!(messages.last(), Some(Message::Fetch { rid: rid2 }));
|
|
||||||
|
|
||||||
// `FetchOk` for the 2nd fetch.
|
|
||||||
alice.receive(bob.id(), Message::FetchOk { rid: rid2 });
|
|
||||||
|
|
||||||
// The 2nd fetch should be in `Io` now. Not the 3rd fetch yet.
|
|
||||||
let last_io = alice.outbox().last().unwrap();
|
|
||||||
assert_matches!(last_io, Io::Fetch(fetch) if fetch.rid == rid2);
|
|
||||||
|
|
||||||
// Finish the 2nd fetch.
|
|
||||||
alice.fetched(
|
|
||||||
Fetch {
|
|
||||||
rid: rid2,
|
|
||||||
direction: FetchDirection::Initiator {
|
|
||||||
namespaces: Namespaces::All,
|
|
||||||
},
|
|
||||||
remote: bob.id,
|
|
||||||
},
|
|
||||||
Ok(vec![]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Now the 2nd fetch is done, the 3rd fetch is drained.
|
|
||||||
let mut messages = alice.messages(bob.id);
|
|
||||||
assert_eq!(messages.next(), Some(Message::Fetch { rid: rid3 }));
|
|
||||||
|
|
||||||
// `FetchOk` for the 3rd fetch.
|
|
||||||
alice.receive(bob.id(), Message::FetchOk { rid: rid3 });
|
|
||||||
|
|
||||||
// The 3rd fetch should be in `Io` now.
|
|
||||||
let last_io = alice.outbox().last().unwrap();
|
|
||||||
assert_matches!(last_io, Io::Fetch(fetch) if fetch.rid == rid3);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
mod frame;
|
||||||
mod message;
|
mod message;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
|
mod varint;
|
||||||
|
|
||||||
|
pub use frame::StreamId;
|
||||||
pub use message::{AddressType, MessageType};
|
pub use message::{AddressType, MessageType};
|
||||||
pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter};
|
pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter};
|
||||||
|
|
||||||
|
|
@ -40,8 +43,14 @@ pub enum Error {
|
||||||
InvalidSize { expected: usize, actual: usize },
|
InvalidSize { expected: usize, actual: usize },
|
||||||
#[error("invalid filter size: {0}")]
|
#[error("invalid filter size: {0}")]
|
||||||
InvalidFilterSize(usize),
|
InvalidFilterSize(usize),
|
||||||
|
#[error("invalid channel type {0:x}")]
|
||||||
|
InvalidStreamKind(u8),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
InvalidRefName(#[from] fmt::Error),
|
InvalidRefName(#[from] fmt::Error),
|
||||||
|
#[error("invalid control message with type `{0}`")]
|
||||||
|
InvalidControlMessage(u8),
|
||||||
|
#[error("invalid protocol version header `{0:x?}`")]
|
||||||
|
InvalidProtocolVersion([u8; 4]),
|
||||||
#[error("unknown address type `{0}`")]
|
#[error("unknown address type `{0}`")]
|
||||||
UnknownAddressType(u8),
|
UnknownAddressType(u8),
|
||||||
#[error("unknown message type `{0}`")]
|
#[error("unknown message type `{0}`")]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
//! Framing protocol.
|
||||||
|
#![warn(clippy::missing_docs_in_private_items)]
|
||||||
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use crate::{wire, wire::varint, wire::varint::VarInt, wire::Message, Link};
|
||||||
|
|
||||||
|
/// Protocol version strings all start with the magic sequence `rad`, followed
|
||||||
|
/// by a version number.
|
||||||
|
pub const PROTOCOL_VERSION: Version = Version([b'r', b'a', b'd', 0x1]);
|
||||||
|
|
||||||
|
/// Control open byte.
|
||||||
|
const CONTROL_OPEN: u8 = 0;
|
||||||
|
/// Control close byte.
|
||||||
|
const CONTROL_CLOSE: u8 = 1;
|
||||||
|
/// Control EOF byte.
|
||||||
|
const CONTROL_EOF: u8 = 2;
|
||||||
|
|
||||||
|
/// Protocol version.
|
||||||
|
pub struct Version([u8; 4]);
|
||||||
|
|
||||||
|
impl wire::Encode for Version {
|
||||||
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||||
|
writer.write_all(&PROTOCOL_VERSION.0)?;
|
||||||
|
|
||||||
|
Ok(PROTOCOL_VERSION.0.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wire::Decode for Version {
|
||||||
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
||||||
|
let mut version = [0u8; 4];
|
||||||
|
reader.read_exact(&mut version[..])?;
|
||||||
|
|
||||||
|
if version != PROTOCOL_VERSION.0 {
|
||||||
|
return Err(wire::Error::InvalidProtocolVersion(version));
|
||||||
|
}
|
||||||
|
Ok(Self(version))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifies a (multiplexed) stream.
|
||||||
|
///
|
||||||
|
/// Stream IDs are variable-length integers with the least significant 3 bits
|
||||||
|
/// denoting the stream type and initiator.
|
||||||
|
///
|
||||||
|
/// The first bit denotes the initiator (outbound or inbound), while the second
|
||||||
|
/// and third bit denote the stream type. See `StreamKind`.
|
||||||
|
///
|
||||||
|
/// In a situation where Alice connects to Bob, Alice will have the initiator
|
||||||
|
/// bit set to `1` for all streams she creates, while Bob will have it set to `0`.
|
||||||
|
///
|
||||||
|
/// This ensures that Stream IDs never collide.
|
||||||
|
/// Additionally, Stream IDs must never be re-used within a connection.
|
||||||
|
///
|
||||||
|
/// +=======+==================================+
|
||||||
|
/// | Bits | Stream Type |
|
||||||
|
/// +=======+==================================+
|
||||||
|
/// | 0b000 | Outbound Control stream |
|
||||||
|
/// +-------+----------------------------------+
|
||||||
|
/// | 0b001 | Inbound Control stream |
|
||||||
|
/// +-------+----------------------------------+
|
||||||
|
/// | 0b010 | Outbound Gossip stream |
|
||||||
|
/// +-------+----------------------------------+
|
||||||
|
/// | 0b011 | Inbound Gossip stream |
|
||||||
|
/// +-------+----------------------------------+
|
||||||
|
/// | 0b100 | Outbound Git stream |
|
||||||
|
/// +-------+----------------------------------+
|
||||||
|
/// | 0b101 | Inbound Git stream |
|
||||||
|
/// +-------+----------------------------------+
|
||||||
|
///
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct StreamId(VarInt);
|
||||||
|
|
||||||
|
impl StreamId {
|
||||||
|
/// Get the initiator of this stream.
|
||||||
|
pub fn link(&self) -> Link {
|
||||||
|
let n = *self.0;
|
||||||
|
if 0b1 & n == 0 {
|
||||||
|
Link::Outbound
|
||||||
|
} else {
|
||||||
|
Link::Inbound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the kind of stream this is.
|
||||||
|
pub fn kind(&self) -> Result<StreamKind, u8> {
|
||||||
|
let id = *self.0;
|
||||||
|
match (id >> 1) & 0b11 {
|
||||||
|
0 => Ok(StreamKind::Control),
|
||||||
|
1 => Ok(StreamKind::Gossip),
|
||||||
|
2 => Ok(StreamKind::Git),
|
||||||
|
n => Err(n as u8),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a control identifier.
|
||||||
|
pub fn control(link: Link) -> Self {
|
||||||
|
match link {
|
||||||
|
Link::Outbound => Self(VarInt::from(0b000u8)),
|
||||||
|
Link::Inbound => Self(VarInt::from(0b001u8)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a gossip identifier.
|
||||||
|
pub fn gossip(link: Link) -> Self {
|
||||||
|
match link {
|
||||||
|
Link::Outbound => Self(VarInt::from(0b010u8)),
|
||||||
|
Link::Inbound => Self(VarInt::from(0b011u8)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a git identifier.
|
||||||
|
pub fn git(link: Link) -> Self {
|
||||||
|
match link {
|
||||||
|
Link::Outbound => Self(VarInt::from(0b100u8)),
|
||||||
|
Link::Inbound => Self(VarInt::from(0b101u8)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the nth identifier while preserving the stream type and initiator.
|
||||||
|
pub fn nth(self, n: u64) -> Result<Self, varint::BoundsExceeded> {
|
||||||
|
let id = *self.0 + (n << 3);
|
||||||
|
VarInt::new(id).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamId> for u64 {
|
||||||
|
fn from(value: StreamId) -> Self {
|
||||||
|
*value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamId> for VarInt {
|
||||||
|
fn from(value: StreamId) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for StreamId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", *self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wire::Decode for StreamId {
|
||||||
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
||||||
|
let id = VarInt::decode(reader)?;
|
||||||
|
Ok(Self(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wire::Encode for StreamId {
|
||||||
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||||
|
self.0.encode(writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type of stream.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum StreamKind {
|
||||||
|
/// Control stream, used to open and close streams.
|
||||||
|
Control,
|
||||||
|
/// Gossip stream, used to exchange messages.
|
||||||
|
Gossip,
|
||||||
|
/// Git stream, used for replication.
|
||||||
|
Git,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Protocol frame.
|
||||||
|
///
|
||||||
|
/// 0 1 2 3
|
||||||
|
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/// | 'r' | 'a' | 'd' | 0x1 | Version
|
||||||
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/// | Stream ID |TTT|I| Stream ID with Stream [T]ype and [I]nitiator bits
|
||||||
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
/// | Data ...| Data (variable size)
|
||||||
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
pub struct Frame {
|
||||||
|
/// The protocol version.
|
||||||
|
pub version: Version,
|
||||||
|
/// The stream identifier.
|
||||||
|
pub stream: StreamId,
|
||||||
|
/// The frame payload.
|
||||||
|
pub data: FrameData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame {
|
||||||
|
/// Create a 'git' protocol frame.
|
||||||
|
pub fn git(stream: StreamId, data: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
|
stream,
|
||||||
|
data: FrameData::Git(data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a 'control' protocol frame.
|
||||||
|
pub fn control(link: Link, ctrl: Control) -> Self {
|
||||||
|
Self {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
|
stream: StreamId::control(link),
|
||||||
|
data: FrameData::Control(ctrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a 'gossip' protocol frame.
|
||||||
|
pub fn gossip(link: Link, msg: Message) -> Self {
|
||||||
|
Self {
|
||||||
|
version: PROTOCOL_VERSION,
|
||||||
|
stream: StreamId::gossip(link),
|
||||||
|
data: FrameData::Gossip(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize frame to bytes.
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
wire::serialize(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame payload.
|
||||||
|
pub enum FrameData {
|
||||||
|
/// Control frame payload.
|
||||||
|
Control(Control),
|
||||||
|
/// Gossip frame payload.
|
||||||
|
Gossip(Message),
|
||||||
|
/// Git frame payload. May contain packet-lines as well as packfile data.
|
||||||
|
Git(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A control message sent over a control stream.
|
||||||
|
pub enum Control {
|
||||||
|
/// Open a new stream.
|
||||||
|
Open {
|
||||||
|
/// The stream to open.
|
||||||
|
stream: StreamId,
|
||||||
|
},
|
||||||
|
/// Close an existing stream.
|
||||||
|
Close {
|
||||||
|
/// The stream to close.
|
||||||
|
stream: StreamId,
|
||||||
|
},
|
||||||
|
/// Signal an end-of-file. This can be used to simulate connections terminating
|
||||||
|
/// without having to close the connection. These control messages are turned into
|
||||||
|
/// [`io::ErrorKind::UnexpectedEof`] errors on read.
|
||||||
|
Eof {
|
||||||
|
/// The stream to send an EOF on.
|
||||||
|
stream: StreamId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wire::Decode for Control {
|
||||||
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
||||||
|
let command = u8::decode(reader)?;
|
||||||
|
match command {
|
||||||
|
CONTROL_OPEN => {
|
||||||
|
let stream = StreamId::decode(reader)?;
|
||||||
|
Ok(Control::Open { stream })
|
||||||
|
}
|
||||||
|
CONTROL_CLOSE => {
|
||||||
|
let stream = StreamId::decode(reader)?;
|
||||||
|
Ok(Control::Close { stream })
|
||||||
|
}
|
||||||
|
CONTROL_EOF => {
|
||||||
|
let stream = StreamId::decode(reader)?;
|
||||||
|
Ok(Control::Eof { stream })
|
||||||
|
}
|
||||||
|
other => Err(wire::Error::InvalidControlMessage(other)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wire::Encode for Control {
|
||||||
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||||
|
let mut n = 0;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Open { stream: id } => {
|
||||||
|
n += CONTROL_OPEN.encode(writer)?;
|
||||||
|
n += id.encode(writer)?;
|
||||||
|
}
|
||||||
|
Self::Eof { stream: id } => {
|
||||||
|
n += CONTROL_EOF.encode(writer)?;
|
||||||
|
n += id.encode(writer)?;
|
||||||
|
}
|
||||||
|
Self::Close { stream: id } => {
|
||||||
|
n += CONTROL_CLOSE.encode(writer)?;
|
||||||
|
n += id.encode(writer)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wire::Decode for Frame {
|
||||||
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
||||||
|
let version = Version::decode(reader)?;
|
||||||
|
let stream = StreamId::decode(reader)?;
|
||||||
|
|
||||||
|
match stream.kind() {
|
||||||
|
Ok(StreamKind::Control) => {
|
||||||
|
let ctrl = Control::decode(reader)?;
|
||||||
|
let frame = Frame {
|
||||||
|
version,
|
||||||
|
stream,
|
||||||
|
data: FrameData::Control(ctrl),
|
||||||
|
};
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
Ok(StreamKind::Gossip) => {
|
||||||
|
let msg = Message::decode(reader)?;
|
||||||
|
let frame = Frame {
|
||||||
|
version,
|
||||||
|
stream,
|
||||||
|
data: FrameData::Gossip(msg),
|
||||||
|
};
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
Ok(StreamKind::Git { .. }) => {
|
||||||
|
let size = VarInt::decode(reader)?;
|
||||||
|
let mut data = vec![0; *size as usize];
|
||||||
|
reader.read_exact(&mut data[..])?;
|
||||||
|
|
||||||
|
Ok(Frame::git(stream, data))
|
||||||
|
}
|
||||||
|
Err(n) => Err(wire::Error::InvalidStreamKind(n)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl wire::Encode for Frame {
|
||||||
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
||||||
|
let mut n = 0;
|
||||||
|
|
||||||
|
n += self.version.encode(writer)?;
|
||||||
|
n += self.stream.encode(writer)?;
|
||||||
|
|
||||||
|
match &self.data {
|
||||||
|
FrameData::Control(ctrl) => {
|
||||||
|
n += ctrl.encode(writer)?;
|
||||||
|
}
|
||||||
|
FrameData::Gossip(msg) => {
|
||||||
|
n += msg.encode(writer)?;
|
||||||
|
}
|
||||||
|
FrameData::Git(data) => {
|
||||||
|
let len = data.len();
|
||||||
|
let size = VarInt::new(len as u64)
|
||||||
|
.map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
|
||||||
|
n += size.encode(writer)?;
|
||||||
|
|
||||||
|
writer.write_all(data.as_slice())?;
|
||||||
|
n += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,6 @@ pub enum MessageType {
|
||||||
Subscribe = 8,
|
Subscribe = 8,
|
||||||
Ping = 10,
|
Ping = 10,
|
||||||
Pong = 12,
|
Pong = 12,
|
||||||
Fetch = 14,
|
|
||||||
FetchOk = 16,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MessageType> for u16 {
|
impl From<MessageType> for u16 {
|
||||||
|
|
@ -40,8 +38,6 @@ impl TryFrom<u16> for MessageType {
|
||||||
8 => Ok(MessageType::Subscribe),
|
8 => Ok(MessageType::Subscribe),
|
||||||
10 => Ok(MessageType::Ping),
|
10 => Ok(MessageType::Ping),
|
||||||
12 => Ok(MessageType::Pong),
|
12 => Ok(MessageType::Pong),
|
||||||
14 => Ok(MessageType::Fetch),
|
|
||||||
16 => Ok(MessageType::FetchOk),
|
|
||||||
_ => Err(other),
|
_ => Err(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,8 +58,6 @@ impl Message {
|
||||||
},
|
},
|
||||||
Self::Ping { .. } => MessageType::Ping,
|
Self::Ping { .. } => MessageType::Ping,
|
||||||
Self::Pong { .. } => MessageType::Pong,
|
Self::Pong { .. } => MessageType::Pong,
|
||||||
Self::Fetch { .. } => MessageType::Fetch,
|
|
||||||
Self::FetchOk { .. } => MessageType::FetchOk,
|
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
@ -216,12 +210,6 @@ impl wire::Encode for Message {
|
||||||
Self::Pong { zeroes } => {
|
Self::Pong { zeroes } => {
|
||||||
n += zeroes.encode(writer)?;
|
n += zeroes.encode(writer)?;
|
||||||
}
|
}
|
||||||
Self::Fetch { rid } => {
|
|
||||||
n += rid.encode(writer)?;
|
|
||||||
}
|
|
||||||
Self::FetchOk { rid } => {
|
|
||||||
n += rid.encode(writer)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if n > wire::Size::MAX as usize {
|
if n > wire::Size::MAX as usize {
|
||||||
|
|
@ -295,14 +283,6 @@ impl wire::Decode for Message {
|
||||||
let zeroes = ZeroBytes::decode(reader)?;
|
let zeroes = ZeroBytes::decode(reader)?;
|
||||||
Ok(Self::Pong { zeroes })
|
Ok(Self::Pong { zeroes })
|
||||||
}
|
}
|
||||||
Ok(MessageType::Fetch) => {
|
|
||||||
let rid = Id::decode(reader)?;
|
|
||||||
Ok(Self::Fetch { rid })
|
|
||||||
}
|
|
||||||
Ok(MessageType::FetchOk) => {
|
|
||||||
let rid = Id::decode(reader)?;
|
|
||||||
Ok(Self::FetchOk { rid })
|
|
||||||
}
|
|
||||||
Err(other) => Err(wire::Error::UnknownMessageType(other)),
|
Err(other) => Err(wire::Error::UnknownMessageType(other)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use std::collections::VecDeque;
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
use std::os::unix::prelude::RawFd;
|
use std::os::unix::prelude::RawFd;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fmt, io, net, str};
|
use std::{io, net};
|
||||||
|
|
||||||
use amplify::Wrapper as _;
|
use amplify::Wrapper as _;
|
||||||
use crossbeam_channel as chan;
|
use crossbeam_channel as chan;
|
||||||
|
|
@ -18,7 +18,7 @@ use cyphernet::{Digest, EcSk, Ecdh, Sha256};
|
||||||
use localtime::LocalTime;
|
use localtime::LocalTime;
|
||||||
use netservices::resource::{ListenerEvent, NetAccept, NetTransport, SessionEvent};
|
use netservices::resource::{ListenerEvent, NetAccept, NetTransport, SessionEvent};
|
||||||
use netservices::session::{ProtocolArtifact, Socks5Session};
|
use netservices::session::{ProtocolArtifact, Socks5Session};
|
||||||
use netservices::{NetConnection, NetProtocol, NetReader, NetSession, NetWriter};
|
use netservices::{NetConnection, NetProtocol, NetReader, NetWriter};
|
||||||
use reactor::Timestamp;
|
use reactor::Timestamp;
|
||||||
|
|
||||||
use radicle::collections::HashMap;
|
use radicle::collections::HashMap;
|
||||||
|
|
@ -27,11 +27,12 @@ use radicle::storage::WriteStorage;
|
||||||
|
|
||||||
use crate::crypto::Signer;
|
use crate::crypto::Signer;
|
||||||
use crate::prelude::Deserializer;
|
use crate::prelude::Deserializer;
|
||||||
use crate::service::reactor::{Fetch, Io};
|
use crate::service::reactor::Io;
|
||||||
use crate::service::{session, DisconnectReason, Message, Service};
|
use crate::service::{session, DisconnectReason, Service};
|
||||||
use crate::wire::{Encode, Error};
|
use crate::wire::frame;
|
||||||
use crate::worker;
|
use crate::wire::frame::{Frame, FrameData, StreamId};
|
||||||
use crate::worker::{Task, TaskResult};
|
use crate::wire::Encode;
|
||||||
|
use crate::worker::{ChannelEvent, Fetch, Task, TaskResult};
|
||||||
use crate::Link;
|
use crate::Link;
|
||||||
use crate::{address, service};
|
use crate::{address, service};
|
||||||
|
|
||||||
|
|
@ -41,22 +42,16 @@ pub const NOISE_XK: HandshakePattern = HandshakePattern {
|
||||||
responder: cyphernet::encrypt::noise::OneWayPattern::Known,
|
responder: cyphernet::encrypt::noise::OneWayPattern::Known,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
/// Control message used internally between workers, users, and the service.
|
/// Control message used internally between workers, users, and the service.
|
||||||
pub enum Control<G: Signer + Ecdh> {
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Control {
|
||||||
/// Message from the user to the service.
|
/// Message from the user to the service.
|
||||||
User(service::Command),
|
User(service::Command),
|
||||||
/// Message from a worker to the service.
|
/// Message from a worker to the service.
|
||||||
Worker(TaskResult<G>),
|
Worker(TaskResult),
|
||||||
}
|
/// Flush data in the given stream to the remote.
|
||||||
|
Flush { remote: NodeId, stream: StreamId },
|
||||||
impl<G: Signer + Ecdh> fmt::Debug for Control<G> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::User(cmd) => cmd.fmt(f),
|
|
||||||
Self::Worker(resp) => resp.result.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peer session type.
|
/// Peer session type.
|
||||||
|
|
@ -69,6 +64,81 @@ pub type WireWriter<G> = NetWriter<NoiseState<G, Sha256>, Socks5Session<net::Tcp
|
||||||
/// Reactor action.
|
/// Reactor action.
|
||||||
type Action<G> = reactor::Action<NetAccept<WireSession<G>>, NetTransport<WireSession<G>>>;
|
type Action<G> = reactor::Action<NetAccept<WireSession<G>>, NetTransport<WireSession<G>>>;
|
||||||
|
|
||||||
|
/// Worker channels used to send Git frames back and forth.
|
||||||
|
struct WorkerChannels {
|
||||||
|
/// Send data to the git worker.
|
||||||
|
sender: chan::Sender<ChannelEvent>,
|
||||||
|
/// Receive data from the git worker.
|
||||||
|
receiver: chan::Receiver<ChannelEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streams associated with a connected peer.
|
||||||
|
struct Streams {
|
||||||
|
/// Active streams and their associated worker channels.
|
||||||
|
/// Note that the gossip and control streams are not included here as they are always
|
||||||
|
/// implied to exist.
|
||||||
|
streams: HashMap<StreamId, WorkerChannels>,
|
||||||
|
/// Connection direction.
|
||||||
|
link: Link,
|
||||||
|
/// Sequence number used to compute the next stream id.
|
||||||
|
seq: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Streams {
|
||||||
|
/// Create a new [`Streams`] object, passing the connection link.
|
||||||
|
fn new(link: Link) -> Self {
|
||||||
|
Self {
|
||||||
|
streams: HashMap::default(),
|
||||||
|
link,
|
||||||
|
seq: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a known stream.
|
||||||
|
fn get(&self, stream: &StreamId) -> Option<&WorkerChannels> {
|
||||||
|
self.streams.get(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a new stream.
|
||||||
|
fn open(&mut self) -> (StreamId, WorkerChannels) {
|
||||||
|
self.seq += 1;
|
||||||
|
|
||||||
|
let id = StreamId::git(self.link)
|
||||||
|
.nth(self.seq)
|
||||||
|
.expect("Streams::open: too many streams");
|
||||||
|
let channels = self
|
||||||
|
.register(id)
|
||||||
|
.expect("Streams::open: stream was already open");
|
||||||
|
|
||||||
|
(id, channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register an open stream.
|
||||||
|
fn register(&mut self, stream: StreamId) -> Option<WorkerChannels> {
|
||||||
|
let (wire_send, wire_recv) = chan::unbounded::<ChannelEvent>();
|
||||||
|
let (work_send, work_recv) = chan::unbounded::<ChannelEvent>();
|
||||||
|
|
||||||
|
match self.streams.entry(stream) {
|
||||||
|
Entry::Vacant(e) => {
|
||||||
|
e.insert(WorkerChannels {
|
||||||
|
sender: wire_send,
|
||||||
|
receiver: work_recv,
|
||||||
|
});
|
||||||
|
Some(WorkerChannels {
|
||||||
|
sender: work_send,
|
||||||
|
receiver: wire_recv,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Entry::Occupied(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unregister an open stream.
|
||||||
|
fn unregister(&mut self, stream: &StreamId) -> Option<WorkerChannels> {
|
||||||
|
self.streams.remove(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Peer connection state machine.
|
/// Peer connection state machine.
|
||||||
enum Peer {
|
enum Peer {
|
||||||
/// The initial state of an inbound peer before handshake is completed.
|
/// The initial state of an inbound peer before handshake is completed.
|
||||||
|
|
@ -79,8 +149,9 @@ enum Peer {
|
||||||
/// Peers in this state are handled by the underlying service.
|
/// Peers in this state are handled by the underlying service.
|
||||||
Connected {
|
Connected {
|
||||||
link: Link,
|
link: Link,
|
||||||
id: NodeId,
|
nid: NodeId,
|
||||||
inbox: Deserializer<Message>,
|
inbox: Deserializer<Frame>,
|
||||||
|
streams: Streams,
|
||||||
},
|
},
|
||||||
/// The peer was scheduled for disconnection. Once the transport is handed over
|
/// The peer was scheduled for disconnection. Once the transport is handed over
|
||||||
/// by the reactor, we can consider it disconnected.
|
/// by the reactor, we can consider it disconnected.
|
||||||
|
|
@ -88,16 +159,6 @@ enum Peer {
|
||||||
id: Option<NodeId>,
|
id: Option<NodeId>,
|
||||||
reason: DisconnectReason,
|
reason: DisconnectReason,
|
||||||
},
|
},
|
||||||
/// The state after we've started the process of upgraded the peer for a fetch.
|
|
||||||
/// The request to handover the socket was made to the reactor.
|
|
||||||
Upgrading {
|
|
||||||
fetch: Fetch,
|
|
||||||
link: Link,
|
|
||||||
id: NodeId,
|
|
||||||
inbox: Vec<u8>,
|
|
||||||
},
|
|
||||||
/// The peer is now upgraded and we are in control of the socket.
|
|
||||||
Upgraded { link: Link, id: NodeId },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Peer {
|
impl std::fmt::Debug for Peer {
|
||||||
|
|
@ -105,16 +166,8 @@ impl std::fmt::Debug for Peer {
|
||||||
match self {
|
match self {
|
||||||
Self::Inbound {} => write!(f, "Inbound"),
|
Self::Inbound {} => write!(f, "Inbound"),
|
||||||
Self::Outbound { id } => write!(f, "Outbound({id})"),
|
Self::Outbound { id } => write!(f, "Outbound({id})"),
|
||||||
Self::Connected { link, id, .. } => write!(f, "Connected({link:?}, {id})"),
|
Self::Connected { link, nid, .. } => write!(f, "Connected({link:?}, {nid})"),
|
||||||
Self::Disconnecting { .. } => write!(f, "Disconnecting"),
|
Self::Disconnecting { .. } => write!(f, "Disconnecting"),
|
||||||
Self::Upgrading {
|
|
||||||
fetch, link, id, ..
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Upgrading(initiated={}, {link:?}, {id})",
|
|
||||||
fetch.is_initiator(),
|
|
||||||
),
|
|
||||||
Self::Upgraded { link, id, .. } => write!(f, "Upgraded({link:?}, {id})"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,11 +177,8 @@ impl Peer {
|
||||||
fn id(&self) -> Option<&NodeId> {
|
fn id(&self) -> Option<&NodeId> {
|
||||||
match self {
|
match self {
|
||||||
Peer::Outbound { id }
|
Peer::Outbound { id }
|
||||||
| Peer::Connected { id, .. }
|
| Peer::Connected { nid: id, .. }
|
||||||
| Peer::Disconnecting { id: Some(id), .. }
|
| Peer::Disconnecting { id: Some(id), .. } => Some(id),
|
||||||
| Peer::Upgrading { id, .. }
|
|
||||||
| Peer::Upgraded { id, .. } => Some(id),
|
|
||||||
|
|
||||||
Peer::Inbound {} => None,
|
Peer::Inbound {} => None,
|
||||||
Peer::Disconnecting { id: None, .. } => None,
|
Peer::Disconnecting { id: None, .. } => None,
|
||||||
}
|
}
|
||||||
|
|
@ -151,8 +201,9 @@ impl Peer {
|
||||||
|
|
||||||
*self = Self::Connected {
|
*self = Self::Connected {
|
||||||
link,
|
link,
|
||||||
id,
|
nid: id,
|
||||||
inbox: Deserializer::default(),
|
inbox: Deserializer::default(),
|
||||||
|
streams: Streams::new(link),
|
||||||
};
|
};
|
||||||
link
|
link
|
||||||
} else if let Self::Outbound { id: expected } = self {
|
} else if let Self::Outbound { id: expected } = self {
|
||||||
|
|
@ -161,8 +212,9 @@ impl Peer {
|
||||||
|
|
||||||
*self = Self::Connected {
|
*self = Self::Connected {
|
||||||
link,
|
link,
|
||||||
id,
|
nid: id,
|
||||||
inbox: Deserializer::default(),
|
inbox: Deserializer::default(),
|
||||||
|
streams: Streams::new(link),
|
||||||
};
|
};
|
||||||
link
|
link
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -172,7 +224,7 @@ impl Peer {
|
||||||
|
|
||||||
/// Switch to disconnecting state.
|
/// Switch to disconnecting state.
|
||||||
fn disconnecting(&mut self, reason: DisconnectReason) {
|
fn disconnecting(&mut self, reason: DisconnectReason) {
|
||||||
if let Self::Connected { id, .. } = self {
|
if let Self::Connected { nid: id, .. } = self {
|
||||||
*self = Self::Disconnecting {
|
*self = Self::Disconnecting {
|
||||||
id: Some(*id),
|
id: Some(*id),
|
||||||
reason,
|
reason,
|
||||||
|
|
@ -188,57 +240,6 @@ impl Peer {
|
||||||
panic!("Peer::disconnected: session is not connected ({self:?})");
|
panic!("Peer::disconnected: session is not connected ({self:?})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to upgrading state.
|
|
||||||
fn upgrading(&mut self, fetch: Fetch) {
|
|
||||||
if let Self::Connected { id, link, inbox } = self {
|
|
||||||
*self = Self::Upgrading {
|
|
||||||
fetch,
|
|
||||||
id: *id,
|
|
||||||
link: *link,
|
|
||||||
inbox: inbox.unparsed().collect(),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
panic!("Peer::upgrading: session is not fully connected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Switch to upgraded state. Returns the unread bytes from the peer.
|
|
||||||
#[must_use]
|
|
||||||
fn upgraded(&mut self) -> (Fetch, Vec<u8>) {
|
|
||||||
if let Self::Upgrading {
|
|
||||||
fetch,
|
|
||||||
id,
|
|
||||||
link,
|
|
||||||
inbox,
|
|
||||||
} = self
|
|
||||||
{
|
|
||||||
let fetch = fetch.clone();
|
|
||||||
let inbox = inbox.drain(..).collect();
|
|
||||||
log::debug!(target: "wire", "Peer {id} upgraded for fetch {}", fetch.rid);
|
|
||||||
|
|
||||||
*self = Self::Upgraded {
|
|
||||||
id: *id,
|
|
||||||
link: *link,
|
|
||||||
};
|
|
||||||
(fetch, inbox)
|
|
||||||
} else {
|
|
||||||
panic!("Peer::upgraded: can't upgrade before handover");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Switch back from upgraded to connected state.
|
|
||||||
fn downgrade(&mut self) {
|
|
||||||
if let Self::Upgraded { id, link, .. } = self {
|
|
||||||
*self = Self::Connected {
|
|
||||||
id: *id,
|
|
||||||
link: *link,
|
|
||||||
inbox: Deserializer::default(),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
panic!("Peer::downgrade: can't downgrade if not in upgraded state");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wire protocol implementation for a set of peers.
|
/// Wire protocol implementation for a set of peers.
|
||||||
|
|
@ -246,7 +247,7 @@ pub struct Wire<R, S, W, G: Signer + Ecdh> {
|
||||||
/// Backing service instance.
|
/// Backing service instance.
|
||||||
service: Service<R, S, W, G>,
|
service: Service<R, S, W, G>,
|
||||||
/// Worker pool interface.
|
/// Worker pool interface.
|
||||||
worker: chan::Sender<Task<G>>,
|
worker: chan::Sender<Task>,
|
||||||
/// Used for authentication.
|
/// Used for authentication.
|
||||||
signer: G,
|
signer: G,
|
||||||
/// Internal queue of actions to send to the reactor.
|
/// Internal queue of actions to send to the reactor.
|
||||||
|
|
@ -266,7 +267,7 @@ where
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
mut service: Service<R, S, W, G>,
|
mut service: Service<R, S, W, G>,
|
||||||
worker: chan::Sender<Task<G>>,
|
worker: chan::Sender<Task>,
|
||||||
signer: G,
|
signer: G,
|
||||||
proxy: net::SocketAddr,
|
proxy: net::SocketAddr,
|
||||||
clock: LocalTime,
|
clock: LocalTime,
|
||||||
|
|
@ -289,13 +290,6 @@ where
|
||||||
self.actions.push_back(Action::RegisterListener(socket));
|
self.actions.push_back(Action::RegisterListener(socket));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn peer_mut_by_fd(&mut self, fd: RawFd) -> &mut Peer {
|
|
||||||
self.peers.get_mut(&fd).unwrap_or_else(|| {
|
|
||||||
log::error!(target: "wire", "Peer with fd {fd} was not found");
|
|
||||||
panic!("Peer with fd {fd} is not known");
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fd_by_id(&self, node_id: &NodeId) -> (RawFd, &Peer) {
|
fn fd_by_id(&self, node_id: &NodeId) -> (RawFd, &Peer) {
|
||||||
self.peers
|
self.peers
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -304,6 +298,14 @@ where
|
||||||
.unwrap_or_else(|| panic!("Peer {node_id} was expected to be known to the transport"))
|
.unwrap_or_else(|| panic!("Peer {node_id} was expected to be known to the transport"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fd_by_id_mut(&mut self, node_id: &NodeId) -> (RawFd, &mut Peer) {
|
||||||
|
self.peers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(_, peer)| peer.id() == Some(node_id))
|
||||||
|
.map(|(fd, peer)| (*fd, peer))
|
||||||
|
.unwrap_or_else(|| panic!("Peer {node_id} was expected to be known to the transport"))
|
||||||
|
}
|
||||||
|
|
||||||
fn connected_fd_by_id(&self, node_id: &NodeId) -> RawFd {
|
fn connected_fd_by_id(&self, node_id: &NodeId) -> RawFd {
|
||||||
match self.fd_by_id(node_id) {
|
match self.fd_by_id(node_id) {
|
||||||
(fd, Peer::Connected { .. }) => fd,
|
(fd, Peer::Connected { .. }) => fd,
|
||||||
|
|
@ -319,16 +321,14 @@ where
|
||||||
self.peers.iter().filter_map(|(fd, peer)| match peer {
|
self.peers.iter().filter_map(|(fd, peer)| match peer {
|
||||||
Peer::Inbound {} => None,
|
Peer::Inbound {} => None,
|
||||||
Peer::Outbound { id } => Some((*fd, id)),
|
Peer::Outbound { id } => Some((*fd, id)),
|
||||||
Peer::Connected { id, .. } => Some((*fd, id)),
|
Peer::Connected { nid: id, .. } => Some((*fd, id)),
|
||||||
Peer::Upgrading { id, .. } => Some((*fd, id)),
|
|
||||||
Peer::Upgraded { id, .. } => Some((*fd, id)),
|
|
||||||
Peer::Disconnecting { .. } => None,
|
Peer::Disconnecting { .. } => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connected(&self) -> impl Iterator<Item = (RawFd, &NodeId)> {
|
fn connected(&self) -> impl Iterator<Item = (RawFd, &NodeId)> {
|
||||||
self.peers.iter().filter_map(|(fd, peer)| {
|
self.peers.iter().filter_map(|(fd, peer)| {
|
||||||
if let Peer::Connected { id, .. } = peer {
|
if let Peer::Connected { nid: id, .. } = peer {
|
||||||
Some((*fd, id))
|
Some((*fd, id))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -353,65 +353,74 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upgrade(&mut self, fd: RawFd, fetch: Fetch) {
|
fn worker_result(&mut self, task: TaskResult) {
|
||||||
let peer = self.peer_mut_by_fd(fd);
|
log::debug!(target: "wire", "Received fetch result from worker: {:?}", task.result);
|
||||||
if let Peer::Disconnecting { .. } = peer {
|
|
||||||
log::error!(target: "wire", "Peer (fd={fd}) is disconnecting");
|
let nid = task.fetch.remote();
|
||||||
|
let Some((fd, peer)) = self
|
||||||
|
.peers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(_, peer)| peer.id() == Some(&nid))
|
||||||
|
.map(|(fd, peer)| (*fd, peer)) else {
|
||||||
|
log::warn!(target: "wire", "Peer {nid} not found; ignoring fetch result");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
log::debug!(target: "wire", "Requesting transport handover from reactor for peer (fd={fd})");
|
|
||||||
peer.upgrading(fetch);
|
|
||||||
|
|
||||||
self.actions.push_back(Action::UnregisterTransport(fd));
|
let Peer::Connected { nid, link, streams, .. } = peer else {
|
||||||
|
log::warn!(target: "wire", "Peer {nid} is not connected; ignoring fetch result");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let remote = *nid;
|
||||||
|
|
||||||
|
// Only call into the service if we initiated this fetch.
|
||||||
|
if let Some((rid, namespaces)) = task.fetch.initiated() {
|
||||||
|
self.service.fetched(rid, namespaces, remote, task.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upgraded(&mut self, transport: NetTransport<WireSession<G>>) {
|
// Nb. It's possible that the stream would already be unregistered if we received an early
|
||||||
let fd = transport.as_raw_fd();
|
// "close" from the remote. Otherwise, we unregister it here and send the "close" ourselves.
|
||||||
let peer = self.peer_mut_by_fd(fd);
|
if streams.unregister(&task.stream).is_some() {
|
||||||
let (fetch, drain) = peer.upgraded();
|
let frame = Frame::control(
|
||||||
let session = match transport.into_session() {
|
*link,
|
||||||
Ok(session) => session,
|
frame::Control::Close {
|
||||||
Err(_) => panic!("Wire::upgraded: peer write buffer not empty on upgrade"),
|
stream: task.stream,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.actions.push_back(Action::Send(fd, frame.to_bytes()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self, remote: NodeId, stream: StreamId) {
|
||||||
|
let (fd, peer) = self
|
||||||
|
.peers
|
||||||
|
.iter()
|
||||||
|
.find(|(_, peer)| peer.id() == Some(&remote))
|
||||||
|
.map(|(fd, peer)| (*fd, peer))
|
||||||
|
.unwrap_or_else(|| panic!("Peer {remote} was expected to be known to the transport"));
|
||||||
|
|
||||||
|
let Peer::Connected { streams, link, .. } = peer else {
|
||||||
|
log::warn!(target: "wire", "Peer {remote} is not connected; ignoring flush");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(c) = streams.get(&stream) else {
|
||||||
|
log::debug!(target: "wire", "Stream {stream} cannot be found; ignoring flush");
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self
|
#[cfg(test)]
|
||||||
.worker
|
if c.receiver.is_empty() {
|
||||||
.send(Task {
|
panic!("Wire:flush: redundant flush");
|
||||||
fetch,
|
|
||||||
session,
|
|
||||||
drain,
|
|
||||||
})
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
log::error!(target: "wire", "Worker pool is disconnected; cannot send fetch request");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn worker_result(&mut self, task: TaskResult<G>) {
|
for data in c.receiver.try_iter() {
|
||||||
log::debug!(target: "wire", "Fetch completed: {:?}", task.result);
|
let frame = match data {
|
||||||
|
ChannelEvent::Data(data) => Frame::git(stream, data),
|
||||||
let session = task.session;
|
ChannelEvent::Close => Frame::control(*link, frame::Control::Close { stream }),
|
||||||
let fd = session.as_connection().as_raw_fd();
|
ChannelEvent::Eof => Frame::control(*link, frame::Control::Eof { stream }),
|
||||||
let peer = self.peer_mut_by_fd(fd);
|
|
||||||
|
|
||||||
let session = if let Peer::Disconnecting { .. } = peer {
|
|
||||||
log::error!(target: "wire", "Peer with fd {fd} is disconnecting");
|
|
||||||
return;
|
|
||||||
} else if let Peer::Upgraded { link, .. } = peer {
|
|
||||||
match NetTransport::with_session(session, *link) {
|
|
||||||
Ok(session) => session,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "wire", "Session downgrade failed: {err}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
todo!();
|
|
||||||
};
|
};
|
||||||
peer.downgrade();
|
self.actions
|
||||||
|
.push_back(reactor::Action::Send(fd, frame.to_bytes()));
|
||||||
self.actions.push_back(Action::RegisterTransport(session));
|
}
|
||||||
self.service.fetched(task.fetch, task.result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,7 +433,7 @@ where
|
||||||
{
|
{
|
||||||
type Listener = NetAccept<WireSession<G>>;
|
type Listener = NetAccept<WireSession<G>>;
|
||||||
type Transport = NetTransport<WireSession<G>>;
|
type Transport = NetTransport<WireSession<G>>;
|
||||||
type Command = Control<G>;
|
type Command = Control;
|
||||||
|
|
||||||
fn tick(&mut self, time: Timestamp) {
|
fn tick(&mut self, time: Timestamp) {
|
||||||
self.service
|
self.service
|
||||||
|
|
@ -508,57 +517,98 @@ where
|
||||||
self.service.connected(id, link);
|
self.service.connected(id, link);
|
||||||
}
|
}
|
||||||
SessionEvent::Data(data) => {
|
SessionEvent::Data(data) => {
|
||||||
if let Some(Peer::Connected { id, inbox, .. }) = self.peers.get_mut(&fd) {
|
if let Some(Peer::Connected {
|
||||||
|
nid,
|
||||||
|
inbox,
|
||||||
|
streams,
|
||||||
|
..
|
||||||
|
}) = self.peers.get_mut(&fd)
|
||||||
|
{
|
||||||
inbox.input(&data);
|
inbox.input(&data);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match inbox.deserialize_next() {
|
match inbox.deserialize_next() {
|
||||||
Ok(Some(msg)) => self.service.received_message(*id, msg),
|
Ok(Some(Frame {
|
||||||
|
data: FrameData::Control(frame::Control::Open { stream }),
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
log::debug!(target: "wire", "Received stream open for id={stream}");
|
||||||
|
|
||||||
|
let Some(WorkerChannels {
|
||||||
|
sender: work_send,
|
||||||
|
receiver: wire_recv,
|
||||||
|
}) = streams.register(stream) else {
|
||||||
|
log::warn!(target: "wire", "Peer attempted to open already-open stream id={stream}");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = Task {
|
||||||
|
fetch: Fetch::Responder { remote: *nid },
|
||||||
|
stream,
|
||||||
|
send: work_send,
|
||||||
|
recv: wire_recv,
|
||||||
|
};
|
||||||
|
if self.worker.send(task).is_err() {
|
||||||
|
log::error!(target: "wire", "Worker pool is disconnected; cannot send task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(Frame {
|
||||||
|
data: FrameData::Control(frame::Control::Eof { stream }),
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
if let Some(channels) = streams.get(&stream) {
|
||||||
|
if channels.sender.send(ChannelEvent::Eof).is_err() {
|
||||||
|
log::error!(target: "wire", "Worker is disconnected; cannot send `EOF`");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "wire", "Ignoring frame on closed or unknown stream id={stream}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(Frame {
|
||||||
|
data: FrameData::Control(frame::Control::Close { stream }),
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
log::debug!(target: "wire", "Received stream close command for id={stream}");
|
||||||
|
|
||||||
|
streams.unregister(&stream);
|
||||||
|
}
|
||||||
|
Ok(Some(Frame {
|
||||||
|
data: FrameData::Gossip(msg),
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
self.service.received_message(*nid, msg);
|
||||||
|
}
|
||||||
|
Ok(Some(Frame {
|
||||||
|
stream,
|
||||||
|
data: FrameData::Git(data),
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
if let Some(channels) = streams.get(&stream) {
|
||||||
|
if channels.sender.send(ChannelEvent::Data(data)).is_err() {
|
||||||
|
log::error!(target: "wire", "Worker is disconnected; cannot send data");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "wire", "Ignoring frame on closed or unknown stream id={stream}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// Buffer is empty, or message isn't complete.
|
// Buffer is empty, or message isn't complete.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "wire", "Invalid gossip message from {id}: {e}");
|
log::error!(target: "wire", "Invalid gossip message from {nid}: {e}");
|
||||||
|
|
||||||
let mut reason =
|
|
||||||
DisconnectReason::Session(session::Error::Misbehavior);
|
|
||||||
if let Error::UnknownMessageType(t) = e {
|
|
||||||
let leftover =
|
|
||||||
inbox.unparsed().chain(t.to_be_bytes()).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if let Ok(header) =
|
|
||||||
str::from_utf8(&leftover[..worker::pktline::HEADER_LEN])
|
|
||||||
{
|
|
||||||
if header.is_ascii() && !header.is_empty() {
|
|
||||||
log::error!(
|
|
||||||
target: "wire",
|
|
||||||
"Received possible Git packet-line header `{}` from {id} (protocol mismatch)",
|
|
||||||
header
|
|
||||||
);
|
|
||||||
// In case of protocol mismatch, don't penalize the peer.
|
|
||||||
reason = DisconnectReason::Session(
|
|
||||||
session::Error::ProtocolMismatch,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !inbox.is_empty() {
|
if !inbox.is_empty() {
|
||||||
log::debug!(target: "wire", "Dropping read buffer for {id} with {} bytes", inbox.unparsed().count());
|
log::debug!(target: "wire", "Dropping read buffer for {nid} with {} bytes", inbox.unparsed().count());
|
||||||
}
|
}
|
||||||
self.disconnect(
|
self.disconnect(
|
||||||
fd, // TODO(cloudhead): Include error in reason.
|
fd,
|
||||||
reason,
|
DisconnectReason::Session(session::Error::Misbehavior),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(Peer::Upgrading { inbox, .. }) = self.peers.get_mut(&fd) {
|
|
||||||
// If somehow the remote peer managed to send git data before the reactor
|
|
||||||
// unregistered our session, we'll hit this branch.
|
|
||||||
inbox.extend(data);
|
|
||||||
} else {
|
} else {
|
||||||
log::warn!(target: "wire", "Dropping message from unconnected peer (fd={fd})");
|
log::warn!(target: "wire", "Dropping message from unconnected peer (fd={fd})");
|
||||||
}
|
}
|
||||||
|
|
@ -573,6 +623,7 @@ where
|
||||||
match cmd {
|
match cmd {
|
||||||
Control::User(cmd) => self.service.command(cmd),
|
Control::User(cmd) => self.service.command(cmd),
|
||||||
Control::Worker(result) => self.worker_result(result),
|
Control::Worker(result) => self.worker_result(result),
|
||||||
|
Control::Flush { remote, stream } => self.flush(remote, stream),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -604,7 +655,11 @@ where
|
||||||
}
|
}
|
||||||
reactor::Error::TransportPollError(fd, _) => {
|
reactor::Error::TransportPollError(fd, _) => {
|
||||||
log::error!(target: "wire", "Received error: peer (fd={fd}) poll error");
|
log::error!(target: "wire", "Received error: peer (fd={fd}) poll error");
|
||||||
self.actions.push_back(Action::UnregisterTransport(*fd));
|
|
||||||
|
self.disconnect(
|
||||||
|
*fd,
|
||||||
|
DisconnectReason::Connection(Arc::new(io::Error::from(io::ErrorKind::Other))),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
reactor::Error::TransportDisconnect(fd, _, _) => {
|
reactor::Error::TransportDisconnect(fd, _, _) => {
|
||||||
log::error!(target: "wire", "Received error: peer (fd={fd}) disconnected");
|
log::error!(target: "wire", "Received error: peer (fd={fd}) disconnected");
|
||||||
|
|
@ -661,9 +716,6 @@ where
|
||||||
}
|
}
|
||||||
e.remove();
|
e.remove();
|
||||||
}
|
}
|
||||||
Peer::Upgrading { .. } => {
|
|
||||||
self.upgraded(transport);
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
panic!("Wire::handover_transport: Unexpected peer with fd {fd} handed over from the reactor");
|
panic!("Wire::handover_transport: Unexpected peer with fd {fd} handed over from the reactor");
|
||||||
}
|
}
|
||||||
|
|
@ -689,8 +741,8 @@ where
|
||||||
while let Some(ev) = self.service.next() {
|
while let Some(ev) = self.service.next() {
|
||||||
match ev {
|
match ev {
|
||||||
Io::Write(node_id, msgs) => {
|
Io::Write(node_id, msgs) => {
|
||||||
let fd = match self.fd_by_id(&node_id) {
|
let (fd, link) = match self.fd_by_id(&node_id) {
|
||||||
(fd, Peer::Connected { .. }) => fd,
|
(fd, Peer::Connected { link, .. }) => (fd, *link),
|
||||||
(_, peer) => {
|
(_, peer) => {
|
||||||
// If the peer is disconnected by the wire protocol, the service may
|
// If the peer is disconnected by the wire protocol, the service may
|
||||||
// not be aware of this yet, and may continue to write messages to it.
|
// not be aware of this yet, and may continue to write messages to it.
|
||||||
|
|
@ -704,7 +756,9 @@ where
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
for msg in msgs {
|
for msg in msgs {
|
||||||
msg.encode(&mut data).expect("in-memory writes never fail");
|
Frame::gossip(link, msg)
|
||||||
|
.encode(&mut data)
|
||||||
|
.expect("in-memory writes never fail");
|
||||||
}
|
}
|
||||||
self.actions.push_back(reactor::Action::Send(fd, data));
|
self.actions.push_back(reactor::Action::Send(fd, data));
|
||||||
}
|
}
|
||||||
|
|
@ -753,10 +807,40 @@ where
|
||||||
Io::Wakeup(d) => {
|
Io::Wakeup(d) => {
|
||||||
self.actions.push_back(reactor::Action::SetTimer(d.into()));
|
self.actions.push_back(reactor::Action::SetTimer(d.into()));
|
||||||
}
|
}
|
||||||
Io::Fetch(fetch) => {
|
Io::Fetch {
|
||||||
// TODO: Check that the node_id is connected, queue request otherwise.
|
rid,
|
||||||
let fd = self.connected_fd_by_id(&fetch.remote);
|
remote,
|
||||||
self.upgrade(fd, fetch);
|
namespaces,
|
||||||
|
} => {
|
||||||
|
log::debug!(target: "wire", "Processing fetch..");
|
||||||
|
|
||||||
|
let (fd, Peer::Connected { link, streams, .. }) =
|
||||||
|
self.fd_by_id_mut(&remote) else {
|
||||||
|
panic!("Wire::next: peer {remote} is not connected");
|
||||||
|
};
|
||||||
|
let (stream, channels) = streams.open();
|
||||||
|
|
||||||
|
log::debug!(target: "wire", "Opened new stream with id={stream} for rid={rid}");
|
||||||
|
|
||||||
|
let link = *link;
|
||||||
|
let task = Task {
|
||||||
|
fetch: Fetch::Initiator {
|
||||||
|
rid,
|
||||||
|
namespaces,
|
||||||
|
remote,
|
||||||
|
},
|
||||||
|
stream,
|
||||||
|
send: channels.sender,
|
||||||
|
recv: channels.receiver,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.worker.send(task).is_err() {
|
||||||
|
log::error!(target: "wire", "Worker pool is disconnected; cannot send fetch request");
|
||||||
|
}
|
||||||
|
self.actions.push_back(Action::Send(
|
||||||
|
fd,
|
||||||
|
Frame::control(link, frame::Control::Open { stream }).to_bytes(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
//! Variable-length integer implementation based on QUIC.
|
||||||
|
#![warn(clippy::missing_docs_in_private_items)]
|
||||||
|
|
||||||
|
// This implementation is largely based on the `quinn` crate.
|
||||||
|
// Copyright (c) 2018 The quinn developers.
|
||||||
|
use std::{fmt, io, ops};
|
||||||
|
|
||||||
|
use byteorder::ReadBytesExt;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::wire;
|
||||||
|
use crate::wire::{Decode, Encode};
|
||||||
|
|
||||||
|
/// An integer less than 2^62
|
||||||
|
///
|
||||||
|
/// Based on QUIC variable-length integers (RFC 9000).
|
||||||
|
///
|
||||||
|
/// > The QUIC variable-length integer encoding reserves the two most significant bits of the first
|
||||||
|
/// > byte to encode the base-2 logarithm of the integer encoding length in bytes. The integer value is
|
||||||
|
/// > encoded on the remaining bits, in network byte order. This means that integers are encoded on 1,
|
||||||
|
/// > 2, 4, or 8 bytes and can encode 6-, 14-, 30-, or 62-bit values, respectively. Table 4 summarizes
|
||||||
|
/// > the encoding properties.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// MSB Length Usable Bits Range
|
||||||
|
/// ----------------------------------------------------
|
||||||
|
/// 00 1 6 0 - 63
|
||||||
|
/// 01 2 14 0 - 16383
|
||||||
|
/// 10 4 30 0 - 1073741823
|
||||||
|
/// 11 8 62 0 - 4611686018427387903
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
pub struct VarInt(pub(crate) u64);
|
||||||
|
|
||||||
|
impl VarInt {
|
||||||
|
/// The largest representable value.
|
||||||
|
pub const MAX: VarInt = VarInt((1 << 62) - 1);
|
||||||
|
|
||||||
|
/// Succeeds iff `x` < 2^62.
|
||||||
|
pub fn new(x: u64) -> Result<Self, BoundsExceeded> {
|
||||||
|
if x <= Self::MAX.0 {
|
||||||
|
Ok(Self(x))
|
||||||
|
} else {
|
||||||
|
Err(BoundsExceeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Deref for VarInt {
|
||||||
|
type Target = u64;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for VarInt {
|
||||||
|
fn from(x: u8) -> Self {
|
||||||
|
VarInt(x.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u16> for VarInt {
|
||||||
|
fn from(x: u16) -> Self {
|
||||||
|
VarInt(x.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for VarInt {
|
||||||
|
fn from(x: u32) -> Self {
|
||||||
|
VarInt(x.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::TryFrom<u64> for VarInt {
|
||||||
|
type Error = BoundsExceeded;
|
||||||
|
/// Succeeds iff `x` < 2^62.
|
||||||
|
fn try_from(x: u64) -> Result<Self, BoundsExceeded> {
|
||||||
|
VarInt::new(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for VarInt {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for VarInt {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error returned when constructing a `VarInt` from a value >= 2^62.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Error)]
|
||||||
|
#[error("value too large for varint encoding")]
|
||||||
|
pub struct BoundsExceeded;
|
||||||
|
|
||||||
|
impl Decode for VarInt {
|
||||||
|
fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, wire::Error> {
|
||||||
|
let mut buf = [0; 8];
|
||||||
|
buf[0] = r.read_u8()?;
|
||||||
|
|
||||||
|
// Integer length.
|
||||||
|
let tag = buf[0] >> 6;
|
||||||
|
buf[0] &= 0b0011_1111;
|
||||||
|
|
||||||
|
let x = match tag {
|
||||||
|
0b00 => u64::from(buf[0]),
|
||||||
|
0b01 => {
|
||||||
|
r.read_exact(&mut buf[1..2])?;
|
||||||
|
u64::from(u16::from_be_bytes([buf[0], buf[1]]))
|
||||||
|
}
|
||||||
|
0b10 => {
|
||||||
|
r.read_exact(&mut buf[1..4])?;
|
||||||
|
u64::from(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
|
||||||
|
}
|
||||||
|
0b11 => {
|
||||||
|
r.read_exact(&mut buf[1..8])?;
|
||||||
|
u64::from_be_bytes(buf)
|
||||||
|
}
|
||||||
|
// SAFETY: It should be obvious that we can't have any other bit pattern
|
||||||
|
// than the above, since all other bits are zeroed.
|
||||||
|
_ => unreachable! {},
|
||||||
|
};
|
||||||
|
Ok(Self(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for VarInt {
|
||||||
|
fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> io::Result<usize> {
|
||||||
|
let x: u64 = self.0;
|
||||||
|
|
||||||
|
if x < 2u64.pow(6) {
|
||||||
|
(x as u8).encode(w)
|
||||||
|
} else if x < 2u64.pow(14) {
|
||||||
|
(0b01 << 14 | x as u16).encode(w)
|
||||||
|
} else if x < 2u64.pow(30) {
|
||||||
|
(0b10 << 30 | x as u32).encode(w)
|
||||||
|
} else if x < 2u64.pow(62) {
|
||||||
|
(0b11 << 62 | x).encode(w)
|
||||||
|
} else {
|
||||||
|
panic!("VarInt::encode: integer overflow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use qcheck_macros::quickcheck;
|
||||||
|
|
||||||
|
impl qcheck::Arbitrary for VarInt {
|
||||||
|
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||||
|
let a = u16::arbitrary(g) as u64;
|
||||||
|
let b = u32::arbitrary(g) as u64;
|
||||||
|
let n = g
|
||||||
|
.choose(&[
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
7,
|
||||||
|
13,
|
||||||
|
37,
|
||||||
|
255,
|
||||||
|
4931,
|
||||||
|
54019,
|
||||||
|
69149,
|
||||||
|
151288809941952652,
|
||||||
|
u8::MAX as u64,
|
||||||
|
u16::MAX as u64,
|
||||||
|
u16::MAX as u64 - 1,
|
||||||
|
u32::MAX as u64,
|
||||||
|
u32::MAX as u64 - 1,
|
||||||
|
*Self::MAX,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
])
|
||||||
|
.copied()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Self(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[quickcheck]
|
||||||
|
fn prop_encode_decode(input: VarInt) {
|
||||||
|
let encoded = wire::serialize(&input);
|
||||||
|
let decoded: VarInt = wire::deserialize(&encoded).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoded, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_encode_overflow() {
|
||||||
|
wire::serialize(&VarInt(u64::MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encoding() {
|
||||||
|
assert_eq!(wire::serialize(&VarInt(0)), vec![0x0]);
|
||||||
|
assert_eq!(wire::serialize(&VarInt(1)), vec![0x01]);
|
||||||
|
assert_eq!(wire::serialize(&VarInt(10)), vec![0x0a]);
|
||||||
|
assert_eq!(wire::serialize(&VarInt(37)), vec![0x25]);
|
||||||
|
assert_eq!(
|
||||||
|
wire::deserialize::<VarInt>(&[0x40, 0x25]).unwrap(),
|
||||||
|
VarInt(37)
|
||||||
|
);
|
||||||
|
assert_eq!(wire::serialize(&VarInt(15293)), vec![0x7b, 0xbd]);
|
||||||
|
assert_eq!(
|
||||||
|
wire::serialize(&VarInt(494878333)),
|
||||||
|
vec![0x9d, 0x7f, 0x3e, 0x7d],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wire::serialize(&VarInt(151288809941952652)),
|
||||||
|
vec![0xc2, 0x19, 0x7c, 0x5e, 0xff, 0x14, 0xe8, 0x8c]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wire::serialize(&VarInt(10000000000)),
|
||||||
|
vec![0xc0, 0x00, 0x00, 0x02, 0x54, 0x0b, 0xe4, 0x00],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
mod channels;
|
||||||
mod fetch;
|
mod fetch;
|
||||||
|
mod tunnel;
|
||||||
|
|
||||||
use std::io::{prelude::*, BufReader};
|
use std::io::{prelude::*, BufReader};
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
|
|
@ -6,20 +8,19 @@ use std::thread::JoinHandle;
|
||||||
use std::{env, io, net, process, thread, time};
|
use std::{env, io, net, process, thread, time};
|
||||||
|
|
||||||
use crossbeam_channel as chan;
|
use crossbeam_channel as chan;
|
||||||
use cyphernet::Ecdh;
|
|
||||||
use netservices::tunnel::Tunnel;
|
|
||||||
use netservices::{AsConnection, NetSession, SplitIo};
|
|
||||||
|
|
||||||
use radicle::crypto::Signer;
|
|
||||||
use radicle::identity::{Id, IdentityError};
|
use radicle::identity::{Id, IdentityError};
|
||||||
|
use radicle::prelude::NodeId;
|
||||||
use radicle::storage::{Namespaces, ReadRepository, RefUpdate};
|
use radicle::storage::{Namespaces, ReadRepository, RefUpdate};
|
||||||
use radicle::{git, Storage};
|
use radicle::{git, Storage};
|
||||||
use reactor::poller::popol;
|
|
||||||
|
|
||||||
use crate::runtime::Handle;
|
use crate::runtime::Handle;
|
||||||
use crate::service::reactor::{Fetch, FetchDirection};
|
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
use crate::wire::{WireReader, WireSession, WireWriter};
|
use crate::wire::StreamId;
|
||||||
|
use channels::{ChannelReader, ChannelWriter, Channels};
|
||||||
|
use tunnel::Tunnel;
|
||||||
|
|
||||||
|
pub use channels::ChannelEvent;
|
||||||
|
|
||||||
/// Worker pool configuration.
|
/// Worker pool configuration.
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
@ -54,8 +55,6 @@ pub enum FetchError {
|
||||||
Identity(#[from] IdentityError),
|
Identity(#[from] IdentityError),
|
||||||
#[error("upload failed: {0}")]
|
#[error("upload failed: {0}")]
|
||||||
Upload(#[from] UploadError),
|
Upload(#[from] UploadError),
|
||||||
#[error("remote aborted fetch")]
|
|
||||||
RemoteAbortedFetch,
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
StagingInit(#[from] fetch::error::Init),
|
StagingInit(#[from] fetch::error::Init),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
@ -76,8 +75,6 @@ impl FetchError {
|
||||||
pub enum UploadError {
|
pub enum UploadError {
|
||||||
#[error("worker failed to connect to git daemon: {0}")]
|
#[error("worker failed to connect to git daemon: {0}")]
|
||||||
DaemonConnectionFailed(io::Error),
|
DaemonConnectionFailed(io::Error),
|
||||||
#[error("git pkt-line command does not match fetch request")]
|
|
||||||
CommandMismatch,
|
|
||||||
#[error("error parsing git command packet-line: {0}")]
|
#[error("error parsing git command packet-line: {0}")]
|
||||||
InvalidPacketLine(io::Error),
|
InvalidPacketLine(io::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
@ -91,33 +88,73 @@ impl UploadError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch job sent to worker thread.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Fetch {
|
||||||
|
/// Client is initiating a fetch in order to receive the specified
|
||||||
|
/// `refspecs` determined by [`Namespaces`].
|
||||||
|
Initiator {
|
||||||
|
/// Repo to fetch.
|
||||||
|
rid: Id,
|
||||||
|
/// Namespaces to fetch.
|
||||||
|
namespaces: Namespaces,
|
||||||
|
/// Remote peer we are interacting with.
|
||||||
|
remote: NodeId,
|
||||||
|
},
|
||||||
|
/// Server is responding to a fetch request by uploading the
|
||||||
|
/// specified `refspecs` sent by the client.
|
||||||
|
Responder {
|
||||||
|
/// Remote peer we are interacting with.
|
||||||
|
remote: NodeId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fetch {
|
||||||
|
pub fn remote(&self) -> NodeId {
|
||||||
|
match self {
|
||||||
|
Self::Initiator { remote, .. } | Self::Responder { remote } => *remote,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initiated(self) -> Option<(Id, Namespaces)> {
|
||||||
|
match self {
|
||||||
|
Self::Initiator {
|
||||||
|
rid, namespaces, ..
|
||||||
|
} => Some((rid, namespaces)),
|
||||||
|
Self::Responder { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 + Ecdh> {
|
pub struct Task {
|
||||||
pub fetch: Fetch,
|
pub fetch: Fetch,
|
||||||
pub session: WireSession<G>,
|
pub stream: StreamId,
|
||||||
pub drain: Vec<u8>,
|
pub send: chan::Sender<ChannelEvent>,
|
||||||
|
pub recv: chan::Receiver<ChannelEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Worker response.
|
/// Worker response.
|
||||||
pub struct TaskResult<G: Signer + Ecdh> {
|
#[derive(Debug)]
|
||||||
|
pub struct TaskResult {
|
||||||
pub fetch: Fetch,
|
pub fetch: Fetch,
|
||||||
|
pub stream: StreamId,
|
||||||
pub result: Result<Vec<RefUpdate>, FetchError>,
|
pub result: Result<Vec<RefUpdate>, FetchError>,
|
||||||
pub session: WireSession<G>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A worker that replicates git objects.
|
/// A worker that replicates git objects.
|
||||||
struct Worker<G: Signer + Ecdh> {
|
struct Worker {
|
||||||
storage: Storage,
|
storage: Storage,
|
||||||
tasks: chan::Receiver<Task<G>>,
|
tasks: chan::Receiver<Task>,
|
||||||
daemon: net::SocketAddr,
|
daemon: net::SocketAddr,
|
||||||
timeout: time::Duration,
|
timeout: time::Duration,
|
||||||
handle: Handle<G>,
|
handle: Handle,
|
||||||
atomic: bool,
|
atomic: bool,
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<G: Signer + Ecdh + 'static> Worker<G> {
|
impl Worker {
|
||||||
/// Waits for tasks and runs them. Blocks indefinitely unless there is an error receiving
|
/// Waits for tasks and runs them. Blocks indefinitely unless there is an error receiving
|
||||||
/// the next task.
|
/// the next task.
|
||||||
fn run(mut self) -> Result<(), chan::RecvError> {
|
fn run(mut self) -> Result<(), chan::RecvError> {
|
||||||
|
|
@ -127,17 +164,15 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(&mut self, task: Task<G>) {
|
fn process(&mut self, task: Task) {
|
||||||
let Task {
|
let Task {
|
||||||
fetch,
|
fetch,
|
||||||
session,
|
recv,
|
||||||
drain,
|
send,
|
||||||
|
stream,
|
||||||
} = task;
|
} = task;
|
||||||
|
let channels = Channels::new(send, recv);
|
||||||
let timeout = session.as_connection().read_timeout().unwrap_or_default();
|
let result = self._process(&fetch, stream, channels);
|
||||||
let (session, result) = self._process(&fetch, drain, session);
|
|
||||||
// In case the timeout is changed during the fetch, we reset it here.
|
|
||||||
session.as_connection().set_read_timeout(timeout).ok();
|
|
||||||
|
|
||||||
log::debug!(target: "worker", "Sending response back to service..");
|
log::debug!(target: "worker", "Sending response back to service..");
|
||||||
|
|
||||||
|
|
@ -145,8 +180,8 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
|
||||||
.handle
|
.handle
|
||||||
.worker_result(TaskResult {
|
.worker_result(TaskResult {
|
||||||
fetch,
|
fetch,
|
||||||
|
stream,
|
||||||
result,
|
result,
|
||||||
session,
|
|
||||||
})
|
})
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
|
|
@ -155,176 +190,135 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _process(
|
fn _process(
|
||||||
&self,
|
&mut self,
|
||||||
fetch: &Fetch,
|
fetch: &Fetch,
|
||||||
drain: Vec<u8>,
|
stream: StreamId,
|
||||||
mut session: WireSession<G>,
|
mut channels: Channels,
|
||||||
) -> (WireSession<G>, Result<Vec<RefUpdate>, FetchError>) {
|
) -> Result<Vec<RefUpdate>, FetchError> {
|
||||||
match &fetch.direction {
|
match &fetch {
|
||||||
FetchDirection::Initiator { namespaces } => {
|
Fetch::Initiator {
|
||||||
log::debug!(target: "worker", "Worker processing outgoing fetch for {}", fetch.rid);
|
rid,
|
||||||
|
namespaces,
|
||||||
|
remote,
|
||||||
|
} => {
|
||||||
|
log::debug!(target: "worker", "Worker processing outgoing fetch for {}", rid);
|
||||||
|
|
||||||
let (session, result) = self.fetch(fetch.rid, namespaces, session);
|
let result = self.fetch(*rid, *remote, stream, namespaces, channels);
|
||||||
if let Err(err) = &result {
|
if let Err(err) = &result {
|
||||||
log::error!(target: "worker", "Fetch error: {err}");
|
log::error!(target: "worker", "Fetch error: {err}");
|
||||||
}
|
}
|
||||||
|
result
|
||||||
(session, result)
|
|
||||||
}
|
}
|
||||||
FetchDirection::Responder => {
|
Fetch::Responder { .. } => {
|
||||||
log::debug!(target: "worker", "Worker processing incoming fetch for {}", fetch.rid);
|
log::debug!(target: "worker", "Worker processing incoming fetch..");
|
||||||
|
|
||||||
if let Err(err) = session.as_connection_mut().set_nonblocking(false) {
|
let (stream_w, mut stream_r) = channels.split();
|
||||||
return (session, Err(err.into()));
|
let mut pktline_r = pktline::Reader::new(&mut stream_r);
|
||||||
|
// Nb. two fetches are usually expected: one for the *special* refs,
|
||||||
|
// followed by another for the signed refs.
|
||||||
|
loop {
|
||||||
|
match self.upload_pack(fetch, stream, &mut pktline_r, stream_w) {
|
||||||
|
Ok(ControlFlow::Continue(())) => continue,
|
||||||
|
Ok(ControlFlow::Break(())) => break,
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut stream_r, mut stream_w) = match session.split_io() {
|
|
||||||
Ok((r, w)) => (r, w),
|
|
||||||
Err(err) => {
|
|
||||||
return (err.original, Err(err.error.into()));
|
|
||||||
}
|
}
|
||||||
};
|
Ok(vec![])
|
||||||
let mut pktline_r = pktline::Reader::new(drain, &mut stream_r);
|
|
||||||
// Nb. two fetches are expected to happen, one for
|
|
||||||
// the `rad` refs, followed by the refs listed in
|
|
||||||
// signed refs.
|
|
||||||
let result = self.upload_pack(fetch, &mut pktline_r, &mut stream_w);
|
|
||||||
let result =
|
|
||||||
result.and_then(|_| self.upload_pack(fetch, &mut pktline_r, &mut stream_w));
|
|
||||||
(WireSession::from_split_io(stream_r, stream_w), result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(
|
fn fetch(
|
||||||
&self,
|
&mut self,
|
||||||
rid: Id,
|
rid: Id,
|
||||||
|
remote: NodeId,
|
||||||
|
stream: StreamId,
|
||||||
namespaces: &Namespaces,
|
namespaces: &Namespaces,
|
||||||
session: WireSession<G>,
|
mut channels: Channels,
|
||||||
) -> (WireSession<G>, Result<Vec<RefUpdate>, FetchError>) {
|
) -> Result<Vec<RefUpdate>, FetchError> {
|
||||||
let staging = match fetch::StagingPhaseInitial::new(&self.storage, rid, namespaces.clone())
|
let staging = fetch::StagingPhaseInitial::new(&self.storage, rid, namespaces.clone())?;
|
||||||
{
|
|
||||||
Ok(staging) => staging,
|
|
||||||
Err(err) => return (session, Err(err.into())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let session = match self.tunnel_fetch(&staging.repo, staging.refspecs(), session) {
|
self._fetch(
|
||||||
(session, Ok(())) => session,
|
&staging.repo,
|
||||||
(session, Err(err)) => return (session, Err(err)),
|
remote,
|
||||||
};
|
staging.refspecs(),
|
||||||
|
stream,
|
||||||
|
&mut channels,
|
||||||
|
)?;
|
||||||
|
if let Err(e) = self.handle.flush(remote, stream) {
|
||||||
|
log::error!(target: "worker", "Error flushing worker stream: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
let staging = match staging.into_final().map_err(FetchError::from) {
|
let staging = match staging.into_final().map_err(FetchError::from) {
|
||||||
Ok(staging) => staging,
|
Ok(staging) => staging,
|
||||||
Err(e) => return (session, Err(e)),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
|
self._fetch(
|
||||||
let (session, res) = self.tunnel_fetch(&staging.repo, staging.refspecs(), session);
|
&staging.repo,
|
||||||
|
remote,
|
||||||
if let Err(e) = res {
|
staging.refspecs(),
|
||||||
return (session, Err(e));
|
stream,
|
||||||
|
&mut channels,
|
||||||
|
)?;
|
||||||
|
if let Err(e) = self.handle.flush(remote, stream) {
|
||||||
|
log::error!(target: "worker", "Error flushing worker stream: {e}");
|
||||||
}
|
}
|
||||||
|
staging.transfer().map_err(FetchError::from)
|
||||||
(session, staging.transfer().map_err(FetchError::from))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn upload_pack(
|
fn upload_pack(
|
||||||
&self,
|
&mut self,
|
||||||
fetch: &Fetch,
|
fetch: &Fetch,
|
||||||
pktline_r: &mut pktline::Reader<WireReader>,
|
stream: StreamId,
|
||||||
stream_w: &mut WireWriter<G>,
|
pktline_r: &mut pktline::Reader<&mut ChannelReader>,
|
||||||
) -> Result<Vec<RefUpdate>, FetchError> {
|
stream_w: &mut ChannelWriter,
|
||||||
match self._upload_pack(fetch, pktline_r, stream_w) {
|
) -> Result<ControlFlow<()>, UploadError> {
|
||||||
Ok(()) => {
|
log::debug!(target: "worker", "Waiting for Git request pktline for..");
|
||||||
log::debug!(target: "worker", "Upload of {} to {} exited successfully", fetch.rid, fetch.remote);
|
|
||||||
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(target: "worker", "Upload error for {}: {err}", fetch.rid);
|
|
||||||
|
|
||||||
// If we exited without receiving a `done` packet, wait for it here.
|
|
||||||
// It's possible that the daemon exited first, or the remote crashed.
|
|
||||||
log::debug!(target: "worker", "Waiting for `done` packet from remote..");
|
|
||||||
let mut header = [0; pktline::HEADER_LEN];
|
|
||||||
|
|
||||||
// Set the read timeout for the `done` packet to twice the configured
|
|
||||||
// value that is used for the fetching (initiator) side.
|
|
||||||
//
|
|
||||||
// This is because the uploader always waits for the `done` packet;
|
|
||||||
// so in case the fetch is aborted by the uploader, eg. if
|
|
||||||
// it can't connect with the daemon, it will wait long enough for the
|
|
||||||
// fetcher to timeout before timing out itself, and will thus receive
|
|
||||||
// the `done` packet.
|
|
||||||
pktline_r
|
|
||||||
.stream()
|
|
||||||
.as_connection()
|
|
||||||
.set_read_timeout(Some(self.timeout * 2))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match pktline_r.read_done_pktline(&mut header) {
|
|
||||||
Ok(()) => {
|
|
||||||
log::debug!(target: "worker", "Received `done` packet from remote");
|
|
||||||
|
|
||||||
// If we get the `done` packet, we exit with the original
|
|
||||||
// error.
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
|
|
||||||
// If we get some other packet, because the fetch request
|
|
||||||
// is still sending stuff, we simply keep reading until we
|
|
||||||
// get a `done` packet.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// If we get any other error, eg. a timeout, we abort.
|
|
||||||
log::error!(
|
|
||||||
target: "worker",
|
|
||||||
"Upload of {} to {} aborted: missing `done` packet from remote",
|
|
||||||
fetch.rid,
|
|
||||||
fetch.remote
|
|
||||||
);
|
|
||||||
return Err(FetchError::RemoteAbortedFetch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _upload_pack(
|
|
||||||
&self,
|
|
||||||
fetch: &Fetch,
|
|
||||||
stream_r: &mut pktline::Reader<WireReader>,
|
|
||||||
stream_w: &mut WireWriter<G>,
|
|
||||||
) -> Result<(), UploadError> {
|
|
||||||
// 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.
|
||||||
let request = match stream_r.read_request_pktline() {
|
let (rid, request) = match pktline_r.read_request_pktline() {
|
||||||
Ok((req, pktline)) => {
|
Ok((req, pktline)) => (req.repo, pktline),
|
||||||
log::debug!(
|
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => {
|
||||||
target: "worker",
|
return Ok(ControlFlow::Break(()));
|
||||||
"Parsed git command packet-line for {}: {:?}", fetch.rid, req
|
|
||||||
);
|
|
||||||
if req.repo != fetch.rid {
|
|
||||||
return Err(UploadError::CommandMismatch);
|
|
||||||
}
|
|
||||||
pktline
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(UploadError::InvalidPacketLine(err));
|
return Err(UploadError::InvalidPacketLine(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
log::debug!(target: "worker", "Received Git request pktline for {rid}..");
|
||||||
|
|
||||||
|
match self._upload_pack(rid, fetch.remote(), request, stream, pktline_r, stream_w) {
|
||||||
|
Ok(()) => {
|
||||||
|
log::debug!(target: "worker", "Upload of {rid} to {} exited successfully", fetch.remote());
|
||||||
|
|
||||||
|
Ok(ControlFlow::Continue(()))
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _upload_pack(
|
||||||
|
&mut self,
|
||||||
|
rid: Id,
|
||||||
|
remote: NodeId,
|
||||||
|
request: Vec<u8>,
|
||||||
|
stream: StreamId,
|
||||||
|
stream_r: &mut pktline::Reader<&mut ChannelReader>,
|
||||||
|
stream_w: &mut ChannelWriter,
|
||||||
|
) -> Result<(), UploadError> {
|
||||||
|
log::debug!(target: "worker", "Connecting to daemon..");
|
||||||
|
|
||||||
// Connect to our local git daemon, running as a child process.
|
// Connect to our local git daemon, running as a child process.
|
||||||
let daemon = net::TcpStream::connect_timeout(&self.daemon, self.timeout)
|
let daemon = net::TcpStream::connect_timeout(&self.daemon, self.timeout)
|
||||||
.map_err(UploadError::DaemonConnectionFailed)?;
|
.map_err(UploadError::DaemonConnectionFailed)?;
|
||||||
let (mut daemon_r, mut daemon_w) = (daemon.try_clone()?, daemon);
|
let (mut daemon_r, mut daemon_w) = (daemon.try_clone()?, daemon);
|
||||||
let mut daemon_r = pktline::Reader::new(vec![], &mut daemon_r);
|
let mut daemon_r = pktline::Reader::new(&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 parsed it.
|
||||||
daemon_w.write_all(&request)?;
|
daemon_w.write_all(&request)?;
|
||||||
|
|
||||||
|
log::debug!(target: "worker", "Entering Git protocol loop for {rid}..");
|
||||||
// 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];
|
let mut buffer = [0; u16::MAX as usize + 1];
|
||||||
|
|
@ -333,23 +327,28 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
|
||||||
if let Err(e) = daemon_r.pipe(stream_w, &mut buffer) {
|
if let Err(e) = daemon_r.pipe(stream_w, &mut buffer) {
|
||||||
// This is the expected error when the daemon disconnects.
|
// This is the expected error when the daemon disconnects.
|
||||||
if e.kind() == io::ErrorKind::UnexpectedEof {
|
if e.kind() == io::ErrorKind::UnexpectedEof {
|
||||||
log::debug!(target: "worker", "Daemon closed the git connection for {}", fetch.rid);
|
log::debug!(target: "worker", "Daemon closed the git connection for {rid}");
|
||||||
}
|
log::debug!(target: "worker", "Waiting for EOF from remote..");
|
||||||
return Err(e.into());
|
|
||||||
}
|
stream_r.wait_for_eof()?;
|
||||||
// Read from the stream and write to the daemon.
|
|
||||||
match stream_r.pipe(&mut daemon_w, &mut buffer) {
|
|
||||||
// Triggered by a [`pktline::DONE_PKT`] packet.
|
|
||||||
Ok(ControlFlow::Break(())) => {
|
|
||||||
log::debug!(target: "worker", "Received `done` packet from remote for {}", fetch.rid);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Ok(ControlFlow::Continue(())) => {
|
return Err(e.into());
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(e) = self.handle.flush(remote, stream) {
|
||||||
|
log::error!(target: "worker", "Worker channel disconnected; aborting");
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from the stream and write to the daemon.
|
||||||
|
match stream_r.pipe(&mut daemon_w, &mut buffer) {
|
||||||
|
Ok(()) => continue,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind() == io::ErrorKind::UnexpectedEof {
|
if e.kind() == io::ErrorKind::ConnectionReset {
|
||||||
log::debug!(target: "worker", "Remote closed the git connection for {}", fetch.rid);
|
log::debug!(target: "worker", "Remote closed the git connection for {rid}");
|
||||||
}
|
}
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
@ -357,41 +356,20 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tunnel_fetch<Specs>(
|
|
||||||
&self,
|
|
||||||
repo: &fetch::StagedRepository,
|
|
||||||
specs: Specs,
|
|
||||||
session: WireSession<G>,
|
|
||||||
) -> (WireSession<G>, Result<(), FetchError>)
|
|
||||||
where
|
|
||||||
Specs: fetch::AsRefspecs,
|
|
||||||
{
|
|
||||||
let mut tunnel = match Tunnel::with(session, net::SocketAddr::from(([0, 0, 0, 0], 0))) {
|
|
||||||
Ok(tunnel) => tunnel,
|
|
||||||
Err((session, err)) => return (session, Err(err.into())),
|
|
||||||
};
|
|
||||||
let res = self._fetch(repo, specs, &mut tunnel);
|
|
||||||
|
|
||||||
let mut session = tunnel.into_session();
|
|
||||||
|
|
||||||
log::debug!(target: "worker", "Sending `done` packet to remote..");
|
|
||||||
if let Err(err) = pktline::done(&mut session) {
|
|
||||||
log::error!(target: "worker", "Fetch error: error sending `done` packet: {err}");
|
|
||||||
}
|
|
||||||
(session, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _fetch<S>(
|
fn _fetch<S>(
|
||||||
&self,
|
&self,
|
||||||
repo: &storage::git::Repository,
|
repo: &storage::git::Repository,
|
||||||
|
remote: NodeId,
|
||||||
specs: S,
|
specs: S,
|
||||||
tunnel: &mut Tunnel<WireSession<G>>,
|
stream: StreamId,
|
||||||
|
channels: &mut Channels,
|
||||||
) -> Result<(), FetchError>
|
) -> Result<(), FetchError>
|
||||||
where
|
where
|
||||||
S: fetch::AsRefspecs,
|
S: fetch::AsRefspecs,
|
||||||
{
|
{
|
||||||
|
let mut tunnel = Tunnel::with(channels, stream, remote, self.handle.clone())?;
|
||||||
let rid = repo.id;
|
let rid = repo.id;
|
||||||
let tunnel_addr = tunnel.local_addr()?;
|
let tunnel_addr = tunnel.local_addr();
|
||||||
let mut cmd = process::Command::new("git");
|
let mut cmd = process::Command::new("git");
|
||||||
cmd.current_dir(repo.path())
|
cmd.current_dir(repo.path())
|
||||||
.env_clear()
|
.env_clear()
|
||||||
|
|
@ -429,10 +407,10 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let _ = tunnel.tunnel_once(popol::Poller::new(), self.timeout)?;
|
tunnel.run(self.timeout)?;
|
||||||
|
|
||||||
let result = child.wait()?;
|
let result = child.wait()?;
|
||||||
if result.success() {
|
let result = if result.success() {
|
||||||
log::debug!(target: "worker", "Fetch for {} exited successfully", rid);
|
log::debug!(target: "worker", "Fetch for {} exited successfully", rid);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -440,7 +418,14 @@ impl<G: Signer + Ecdh + 'static> Worker<G> {
|
||||||
Err(FetchError::CommandFailed {
|
Err(FetchError::CommandFailed {
|
||||||
code: result.code().unwrap_or(1),
|
code: result.code().unwrap_or(1),
|
||||||
})
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!(target: "worker", "Sending `EOF` to remote..");
|
||||||
|
|
||||||
|
if let Err(e) = channels.sender.eof() {
|
||||||
|
log::error!(target: "worker", "Fetch error: error sending `EOF` message: {e}");
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,11 +436,7 @@ pub struct Pool {
|
||||||
|
|
||||||
impl Pool {
|
impl Pool {
|
||||||
/// Create a new worker pool with the given parameters.
|
/// Create a new worker pool with the given parameters.
|
||||||
pub fn with<G: Signer + Ecdh + 'static>(
|
pub fn with(tasks: chan::Receiver<Task>, handle: Handle, config: Config) -> Self {
|
||||||
tasks: chan::Receiver<Task<G>>,
|
|
||||||
handle: Handle<G>,
|
|
||||||
config: Config,
|
|
||||||
) -> Self {
|
|
||||||
let mut pool = Vec::with_capacity(config.capacity);
|
let mut pool = Vec::with_capacity(config.capacity);
|
||||||
for _ in 0..config.capacity {
|
for _ in 0..config.capacity {
|
||||||
let worker = Worker {
|
let worker = Worker {
|
||||||
|
|
@ -495,7 +476,7 @@ impl Pool {
|
||||||
pub mod pktline {
|
pub mod pktline {
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::ops::ControlFlow;
|
use std::net::TcpStream;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
use super::Id;
|
use super::Id;
|
||||||
|
|
@ -504,84 +485,69 @@ pub mod pktline {
|
||||||
pub const FLUSH_PKT: &[u8; HEADER_LEN] = b"0000";
|
pub const FLUSH_PKT: &[u8; HEADER_LEN] = b"0000";
|
||||||
pub const DELIM_PKT: &[u8; HEADER_LEN] = b"0001";
|
pub const DELIM_PKT: &[u8; HEADER_LEN] = b"0001";
|
||||||
pub const RESPONSE_END_PKT: &[u8; HEADER_LEN] = b"0002";
|
pub const RESPONSE_END_PKT: &[u8; HEADER_LEN] = b"0002";
|
||||||
/// When the remote `fetch` exits, it sends a special `done` packet which triggers
|
|
||||||
/// an EOF. This `done` packet is not part of the git protocol, and so is
|
|
||||||
/// not sent to the deamon.
|
|
||||||
pub const DONE_PKT: &[u8; HEADER_LEN] = b"done";
|
|
||||||
|
|
||||||
/// Packetline read result.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Packetline {
|
|
||||||
/// Received a `done` control packet.
|
|
||||||
Done,
|
|
||||||
/// Received a git packet with the given length.
|
|
||||||
Git(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a special `done` packet. Since the git protocol is tunneled over an existing
|
|
||||||
/// connection, we can't signal the end of the protocol via the usual means, which is
|
|
||||||
/// to close the connection and trigger an EOF on the other side. Git also doesn't have
|
|
||||||
/// any special message we can send to signal the end of the protocol. Hence, we there's
|
|
||||||
/// no other way for the server to know that we're done sending commands than to send a
|
|
||||||
/// message that is not part of the git protocol. This message can then be processed by
|
|
||||||
/// the remote worker to end the protocol.
|
|
||||||
pub fn done<W: io::Write>(w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(DONE_PKT)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Reader<'a, R> {
|
pub struct Reader<'a, R> {
|
||||||
drain: Vec<u8>,
|
|
||||||
stream: &'a mut R,
|
stream: &'a mut R,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Reader<'a, TcpStream> {
|
||||||
|
/// Check whether the stream ended.
|
||||||
|
pub fn is_eof(&self) -> io::Result<bool> {
|
||||||
|
// Use non-blocking mode instead of timeouts, as we don't want to mess
|
||||||
|
// with existing timeouts.
|
||||||
|
self.stream.set_nonblocking(true)?;
|
||||||
|
let eof = match self.stream.peek(&mut []) {
|
||||||
|
Ok(0) => true,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
self.stream.set_nonblocking(false)?;
|
||||||
|
|
||||||
|
Ok(eof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, R: io::Read> Reader<'a, R> {
|
impl<'a, R: io::Read> Reader<'a, R> {
|
||||||
/// Create a new packet-line reader.
|
/// Create a new packet-line reader.
|
||||||
pub fn new(drain: Vec<u8>, stream: &'a mut R) -> Self {
|
pub fn new(stream: &'a mut R) -> Self {
|
||||||
Self { drain, stream }
|
Self { stream }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the underlying stream.
|
/// Get the underlying stream.
|
||||||
pub fn stream(&self) -> &R {
|
pub fn stream(&mut self) -> &mut R {
|
||||||
self.stream
|
self.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wait for EOF.
|
||||||
|
pub fn wait_for_eof(&mut self) -> io::Result<()> {
|
||||||
|
match self.stream.read_to_end(&mut Vec::new()) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a Git request packet-line.
|
/// Parse a Git request packet-line.
|
||||||
///
|
///
|
||||||
/// Example: `0032git-upload-pack /project.git\0host=myserver.com\0`
|
/// Example: `0032git-upload-pack /project.git\0host=myserver.com\0`
|
||||||
///
|
///
|
||||||
pub fn read_request_pktline(&mut self) -> io::Result<(GitRequest, Vec<u8>)> {
|
pub fn read_request_pktline(&mut self) -> io::Result<(GitRequest, Vec<u8>)> {
|
||||||
let mut pktline = [0u8; 1024];
|
let mut pktline = [0u8; 1024];
|
||||||
let Packetline::Git(length) = self.read_pktline(&mut pktline)? else {
|
let length = self.read_pktline(&mut pktline)?;
|
||||||
return Err(io::ErrorKind::InvalidInput.into());
|
|
||||||
};
|
|
||||||
let Some(cmd) = GitRequest::parse(&pktline[4..length]) else {
|
let Some(cmd) = GitRequest::parse(&pktline[4..length]) else {
|
||||||
return Err(io::ErrorKind::InvalidInput.into());
|
return Err(io::ErrorKind::InvalidInput.into());
|
||||||
};
|
};
|
||||||
Ok((cmd, Vec::from(&pktline[..length])))
|
Ok((cmd, Vec::from(&pktline[..length])))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a `done` packet-line.
|
|
||||||
pub fn read_done_pktline(&mut self, buf: &mut [u8]) -> io::Result<()> {
|
|
||||||
self.read_exact(&mut buf[..HEADER_LEN])?;
|
|
||||||
|
|
||||||
if &buf[..HEADER_LEN] == DONE_PKT {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(io::ErrorKind::InvalidInput.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a Git packet-line.
|
/// Parse a Git packet-line.
|
||||||
pub fn read_pktline(&mut self, buf: &mut [u8]) -> io::Result<Packetline> {
|
pub fn read_pktline(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
self.read_exact(&mut buf[..HEADER_LEN])?;
|
self.read_exact(&mut buf[..HEADER_LEN])?;
|
||||||
|
|
||||||
if &buf[..HEADER_LEN] == DONE_PKT {
|
|
||||||
return Ok(Packetline::Done);
|
|
||||||
}
|
|
||||||
if &buf[..HEADER_LEN] == FLUSH_PKT
|
if &buf[..HEADER_LEN] == FLUSH_PKT
|
||||||
|| &buf[..HEADER_LEN] == DELIM_PKT
|
|| &buf[..HEADER_LEN] == DELIM_PKT
|
||||||
|| &buf[..HEADER_LEN] == RESPONSE_END_PKT
|
|| &buf[..HEADER_LEN] == RESPONSE_END_PKT
|
||||||
{
|
{
|
||||||
return Ok(Packetline::Git(HEADER_LEN));
|
return Ok(HEADER_LEN);
|
||||||
}
|
}
|
||||||
let length = str::from_utf8(&buf[..HEADER_LEN])
|
let length = str::from_utf8(&buf[..HEADER_LEN])
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?;
|
||||||
|
|
@ -590,25 +556,14 @@ pub mod pktline {
|
||||||
|
|
||||||
self.read_exact(&mut buf[HEADER_LEN..length])?;
|
self.read_exact(&mut buf[HEADER_LEN..length])?;
|
||||||
|
|
||||||
Ok(Packetline::Git(length))
|
Ok(length)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read packet-lines from the internal reader into `buf`,
|
/// Read packet-lines from the internal reader into `buf`,
|
||||||
/// and write them to the given writer.
|
/// and write them to the given writer. Exits when a [`FLUSH_PKT`] packet is received.
|
||||||
///
|
pub fn pipe<W: io::Write>(&mut self, w: &mut W, buf: &mut [u8]) -> io::Result<()> {
|
||||||
/// Returns [`ControlFlow::Break`] if the fetch should be terminated.
|
|
||||||
/// Otherwise, returns [`ControlFlow::Continue`] to mean that we're
|
|
||||||
/// expecting a response from the remote.
|
|
||||||
pub fn pipe<W: io::Write>(
|
|
||||||
&mut self,
|
|
||||||
w: &mut W,
|
|
||||||
buf: &mut [u8],
|
|
||||||
) -> io::Result<ControlFlow<()>> {
|
|
||||||
loop {
|
loop {
|
||||||
let n = match self.read_pktline(buf)? {
|
let n = self.read_pktline(buf)?;
|
||||||
Packetline::Done => return Ok(ControlFlow::Break(())),
|
|
||||||
Packetline::Git(n) => n,
|
|
||||||
};
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -618,19 +573,12 @@ pub mod pktline {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ControlFlow::Continue(()))
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, R: io::Read> io::Read for Reader<'a, R> {
|
impl<'a, R: io::Read> io::Read for Reader<'a, R> {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
if !self.drain.is_empty() {
|
|
||||||
let count = buf.len().min(self.drain.len());
|
|
||||||
buf[..count].copy_from_slice(&self.drain[..count]);
|
|
||||||
self.drain.drain(..count);
|
|
||||||
|
|
||||||
return Ok(count);
|
|
||||||
}
|
|
||||||
self.stream.read(buf)
|
self.stream.read(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use crossbeam_channel as chan;
|
||||||
|
|
||||||
|
/// Data that can be sent and received on worker channels.
|
||||||
|
pub enum ChannelEvent<T = Vec<u8>> {
|
||||||
|
/// Git protocol data.
|
||||||
|
Data(T),
|
||||||
|
/// A request to close the channel.
|
||||||
|
Close,
|
||||||
|
/// A signal that the git protocol has ended, eg. when the remote fetch closes the
|
||||||
|
/// connection.
|
||||||
|
Eof,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for ChannelEvent<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Data(_) => write!(f, "ChannelEvent::Data(..)"),
|
||||||
|
Self::Close => write!(f, "ChannelEvent::Close"),
|
||||||
|
Self::Eof => write!(f, "ChannelEvent::Eof"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Worker channels for communicating through the git stream with the remote.
|
||||||
|
pub struct Channels<T = Vec<u8>> {
|
||||||
|
pub sender: ChannelWriter<T>,
|
||||||
|
pub receiver: ChannelReader<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Channels {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.sender.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.sender.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for Channels {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.receiver.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Channels<T> {
|
||||||
|
pub fn new(
|
||||||
|
sender: chan::Sender<ChannelEvent<T>>,
|
||||||
|
receiver: chan::Receiver<ChannelEvent<T>>,
|
||||||
|
) -> Self {
|
||||||
|
Channels {
|
||||||
|
sender: ChannelWriter(sender),
|
||||||
|
receiver: ChannelReader {
|
||||||
|
receiver,
|
||||||
|
buffer: io::Cursor::new(Vec::new()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split(&mut self) -> (&mut ChannelWriter<T>, &mut ChannelReader<T>) {
|
||||||
|
(&mut self.sender, &mut self.receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a [`chan::Receiver`] and provides it with [`io::Read`].
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ChannelReader<T = Vec<u8>> {
|
||||||
|
buffer: io::Cursor<Vec<u8>>,
|
||||||
|
receiver: chan::Receiver<ChannelEvent<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for ChannelReader<Vec<u8>> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let read = self.buffer.read(buf)?;
|
||||||
|
if read == 0 {
|
||||||
|
let event = self.receiver.recv().map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::BrokenPipe,
|
||||||
|
"error reading from stream: channel is disconnected",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
ChannelEvent::Data(data) => {
|
||||||
|
self.buffer = io::Cursor::new(data);
|
||||||
|
self.buffer.read(buf)
|
||||||
|
}
|
||||||
|
ChannelEvent::Eof => Err(io::ErrorKind::UnexpectedEof.into()),
|
||||||
|
ChannelEvent::Close => Err(io::ErrorKind::ConnectionReset.into()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a [`chan::Sender`] and provides it with [`io::Write`].
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ChannelWriter<T = Vec<u8>>(chan::Sender<ChannelEvent<T>>);
|
||||||
|
|
||||||
|
impl ChannelWriter {
|
||||||
|
/// Since the git protocol is tunneled over an existing connection, we can't signal the end of
|
||||||
|
/// the protocol via the usual means, which is to close the connection. Git also doesn't have
|
||||||
|
/// any special message we can send to signal the end of the protocol.
|
||||||
|
///
|
||||||
|
/// Hence, we there's no other way for the server to know that we're done sending requests
|
||||||
|
/// than to send a special message outside the git protocol. This message can then be processed
|
||||||
|
/// by the remote worker to end the protocol. We use the special "eof" control message for this.
|
||||||
|
pub fn eof(&self) -> Result<(), chan::SendError<ChannelEvent>> {
|
||||||
|
self.0.send(ChannelEvent::Eof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for ChannelWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let data = buf.to_vec();
|
||||||
|
self.0.send(ChannelEvent::Data(data)).map_err(|m| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::BrokenPipe,
|
||||||
|
format!(
|
||||||
|
"error writing to stream: channel is disconnected: dropped {:?}",
|
||||||
|
m.into_inner()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
net, time,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::channels::Channels;
|
||||||
|
use super::{pktline, Handle, NodeId, StreamId};
|
||||||
|
|
||||||
|
/// Tunnels fetches to a remote peer.
|
||||||
|
pub struct Tunnel<'a> {
|
||||||
|
stream: &'a mut Channels,
|
||||||
|
listener: net::TcpListener,
|
||||||
|
local_addr: net::SocketAddr,
|
||||||
|
channel: StreamId,
|
||||||
|
remote: NodeId,
|
||||||
|
handle: Handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Tunnel<'a> {
|
||||||
|
pub(super) fn with(
|
||||||
|
stream: &'a mut Channels,
|
||||||
|
channel: StreamId,
|
||||||
|
remote: NodeId,
|
||||||
|
handle: Handle,
|
||||||
|
) -> io::Result<Self> {
|
||||||
|
let listener = net::TcpListener::bind(net::SocketAddr::from(([0, 0, 0, 0], 0)))?;
|
||||||
|
let local_addr = listener.local_addr()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
stream,
|
||||||
|
listener,
|
||||||
|
local_addr,
|
||||||
|
channel,
|
||||||
|
remote,
|
||||||
|
handle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_addr(&self) -> net::SocketAddr {
|
||||||
|
self.local_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the tunnel until the connection is closed.
|
||||||
|
pub fn run(&mut self, timeout: time::Duration) -> io::Result<()> {
|
||||||
|
// 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`).
|
||||||
|
let mut buffer = [0; u16::MAX as usize + 1];
|
||||||
|
let (mut remote_w, mut remote_r) = self.stream.split();
|
||||||
|
let (mut stream, _) = self.listener.accept()?;
|
||||||
|
|
||||||
|
let mut local = pktline::Reader::new(&mut stream);
|
||||||
|
let mut remote_r = pktline::Reader::new(&mut remote_r);
|
||||||
|
|
||||||
|
local.stream().set_read_timeout(Some(timeout))?;
|
||||||
|
local.stream().set_write_timeout(Some(timeout))?;
|
||||||
|
|
||||||
|
let (_, buf) = local.read_request_pktline()?;
|
||||||
|
remote_w.write_all(&buf)?;
|
||||||
|
|
||||||
|
// Nb. Annoyingly, we have to always check if the fetch stream is closed on every
|
||||||
|
// iteration, otherwise we may get stuck waiting for data from the remote while
|
||||||
|
// we're actually done. After measurement, this checking for EOF only takes
|
||||||
|
// between 1µs and 4µs, and is therefore an okay compromise.
|
||||||
|
while !local.is_eof()? {
|
||||||
|
if self.handle.flush(self.remote, self.channel).is_err() {
|
||||||
|
return Err(io::ErrorKind::BrokenPipe.into());
|
||||||
|
}
|
||||||
|
remote_r.pipe(local.stream(), &mut buffer)?;
|
||||||
|
|
||||||
|
if let Err(e) = local.pipe(&mut remote_w, &mut buffer) {
|
||||||
|
// This is the expected error when the git fetch closes the connection.
|
||||||
|
if e.kind() == io::ErrorKind::UnexpectedEof {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue