node: Fully integrate worker fetch into service
This commit is contained in:
parent
ec2e7d7ee1
commit
ef950d515a
|
|
@ -7,7 +7,7 @@ pub enum Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A vector with an upper limit on its size using type level constants.
|
/// A vector with an upper limit on its size using type level constants.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Default, Clone, PartialEq, Eq)]
|
||||||
pub struct BoundedVec<T, const N: usize> {
|
pub struct BoundedVec<T, const N: usize> {
|
||||||
v: Vec<T>,
|
v: Vec<T>,
|
||||||
}
|
}
|
||||||
|
|
@ -186,3 +186,9 @@ impl<T, const N: usize> TryFrom<Vec<T>> for BoundedVec<T, N> {
|
||||||
Ok(BoundedVec { v: value })
|
Ok(BoundedVec { v: value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: std::fmt::Debug, const N: usize> std::fmt::Debug for BoundedVec<T, N> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.v.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,13 @@ use crate::service::message::{Announcement, AnnouncementMessage, Ping};
|
||||||
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
|
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
|
||||||
use crate::service::session::Protocol;
|
use crate::service::session::Protocol;
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteRepository, WriteStorage};
|
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteStorage};
|
||||||
use crate::Link;
|
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;
|
||||||
|
|
@ -79,8 +80,8 @@ pub use message::REF_LIMIT;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
RefsFetched {
|
RefsFetched {
|
||||||
from: NodeId,
|
remote: NodeId,
|
||||||
project: Id,
|
rid: Id,
|
||||||
updated: Vec<RefUpdate>,
|
updated: Vec<RefUpdate>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +135,7 @@ pub enum FetchLookup {
|
||||||
pub struct FetchResult {
|
pub struct FetchResult {
|
||||||
pub rid: Id,
|
pub rid: Id,
|
||||||
pub remote: NodeId,
|
pub remote: NodeId,
|
||||||
|
pub namespaces: Namespaces,
|
||||||
pub result: Result<Vec<RefUpdate>, FetchError>,
|
pub result: Result<Vec<RefUpdate>, FetchError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -473,9 +475,7 @@ where
|
||||||
|
|
||||||
// TODO: Limit the number of seeds we fetch from? Randomize?
|
// TODO: Limit the number of seeds we fetch from? Randomize?
|
||||||
for seed in seeds {
|
for seed in seeds {
|
||||||
if let Err(err) = self.fetch(id, seed) {
|
self.fetch(id, &seed);
|
||||||
log::error!("Error initiating fetch for {id} from {seed}: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TrackRepo(id, resp) => {
|
Command::TrackRepo(id, resp) => {
|
||||||
|
|
@ -515,33 +515,51 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(&mut self, rid: Id, seed: NodeId) -> Result<(), Error> {
|
pub fn fetch(&mut self, rid: Id, seed: &NodeId) {
|
||||||
let session = self.sessions.get_mut(&seed).unwrap();
|
let Some(session) = self.sessions.get_mut(seed) else {
|
||||||
|
panic!("Service::fetch: attempted to fetch from unknown peer {seed}");
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(fetch) = session.fetch(rid) {
|
if let Some(fetch) = session.fetch(rid) {
|
||||||
|
debug!("Fetch initiated for {rid} with {seed}..");
|
||||||
|
|
||||||
self.reactor.write(session.id, fetch);
|
self.reactor.write(session.id, fetch);
|
||||||
self.reactor
|
|
||||||
.fetch(session.id, rid, Namespaces::default(), true);
|
|
||||||
} else {
|
} else {
|
||||||
// TODO: If we can't fetch, it's because we're already fetching from
|
// TODO: If we can't fetch, it's because we're already fetching from
|
||||||
// this peer. So we need to queue the request, or find another peer.
|
// this peer. So we need to queue the request, or find another peer.
|
||||||
todo!();
|
log::error!(
|
||||||
|
"Unable to fetch {rid} from peer {seed} that is already being fetched from"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repo_fetched(&mut self, result: FetchResult) {
|
pub fn fetched(&mut self, result: FetchResult) {
|
||||||
let remote = result.remote;
|
let remote = result.remote;
|
||||||
let rid = result.rid;
|
let rid = result.rid;
|
||||||
|
|
||||||
|
match &result.result {
|
||||||
|
Ok(updated) => {
|
||||||
|
self.reactor.event(Event::RefsFetched {
|
||||||
|
remote,
|
||||||
|
rid,
|
||||||
|
updated: updated.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Fetch failed for {rid} from {remote}: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(results) = self.fetch_reqs.get(&rid) {
|
if let Some(results) = self.fetch_reqs.get(&rid) {
|
||||||
if results.send(result).is_err() {
|
if results.send(result).is_err() {
|
||||||
self.fetch_reqs.remove(&rid);
|
self.fetch_reqs.remove(&rid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(session) = self.sessions.get_mut(&remote) {
|
if let Some(session) = self.sessions.get_mut(&remote) {
|
||||||
if let session::State::Connected { protocol, .. } = &mut session.state {
|
if let session::State::Connected { protocol, .. } = &mut session.state {
|
||||||
if *protocol == session::Protocol::Fetch {
|
if *protocol == session::Protocol::Fetch {
|
||||||
*protocol = session::Protocol::Gossip;
|
*protocol = session::Protocol::default();
|
||||||
} else {
|
} else {
|
||||||
panic!(
|
panic!(
|
||||||
"Unexpected session state for {}: expected 'fetch' protocol, got 'gossip'",
|
"Unexpected session state for {}: expected 'fetch' protocol, got 'gossip'",
|
||||||
|
|
@ -734,32 +752,9 @@ where
|
||||||
// Refs are only supposed to be relayed by peers who are tracking
|
// Refs are only supposed to be relayed by peers who are tracking
|
||||||
// the resource. Therefore, it's safe to fetch from the remote
|
// the resource. Therefore, it's safe to fetch from the remote
|
||||||
// peer, even though it isn't the announcer.
|
// peer, even though it isn't the announcer.
|
||||||
let updated = match self
|
self.fetch(message.id, relayer);
|
||||||
.storage
|
|
||||||
.repository(message.id)
|
|
||||||
.map_err(storage::FetchError::from)
|
|
||||||
.and_then(|mut r| r.fetch(relayer, Namespaces::default()))
|
|
||||||
{
|
|
||||||
Ok(updated) => updated,
|
|
||||||
Err(err) => {
|
|
||||||
error!(
|
|
||||||
"Error fetching repository {} from {}: {}",
|
|
||||||
message.id, relayer, err
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let is_updated = !updated.is_empty();
|
|
||||||
|
|
||||||
self.reactor.event(Event::RefsFetched {
|
return Ok(true);
|
||||||
from: *relayer,
|
|
||||||
project: message.id,
|
|
||||||
updated,
|
|
||||||
});
|
|
||||||
|
|
||||||
if is_updated {
|
|
||||||
return Ok(relay);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Ignoring refs announcement from {announcer}: repository {} isn't tracked",
|
"Ignoring refs announcement from {announcer}: repository {} isn't tracked",
|
||||||
|
|
@ -844,14 +839,14 @@ where
|
||||||
match (&mut peer.state, message) {
|
match (&mut peer.state, message) {
|
||||||
(
|
(
|
||||||
session::State::Connected {
|
session::State::Connected {
|
||||||
protocol: session::Protocol::Fetch { .. },
|
protocol: session::Protocol::Fetch,
|
||||||
..
|
..
|
||||||
},
|
},
|
||||||
_,
|
_,
|
||||||
) => {
|
) => {
|
||||||
// This should never happen if the service is properly configured, since all
|
// This should never happen if the service is properly configured, since all
|
||||||
// incoming data is sent directly to the Git worker.
|
// incoming data is sent directly to the Git worker.
|
||||||
log::error!("Received gossip message from {remote} during Git fetch");
|
log::error!("Received gossip message from {remote} during git fetch");
|
||||||
|
|
||||||
return Err(session::Error::Misbehavior);
|
return Err(session::Error::Misbehavior);
|
||||||
}
|
}
|
||||||
|
|
@ -933,11 +928,37 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(session::State::Connected { protocol, .. }, Message::Fetch { repo }) => {
|
(session::State::Connected { protocol, .. }, Message::Fetch { rid }) => {
|
||||||
|
debug!("Fetch requested for {rid} from {remote}..");
|
||||||
|
|
||||||
|
// TODO: Check that we have the repo first?
|
||||||
|
|
||||||
|
*protocol = Protocol::Fetch;
|
||||||
|
// Accept the request and instruct the transport to handover the socket to the worker.
|
||||||
|
self.reactor.write(*remote, Message::FetchOk { rid });
|
||||||
|
self.reactor
|
||||||
|
.fetch(*remote, rid, Namespaces::default(), false);
|
||||||
|
}
|
||||||
|
(session::State::Connected { protocol, .. }, Message::FetchOk { rid }) => {
|
||||||
|
if *protocol
|
||||||
|
!= (session::Protocol::Gossip {
|
||||||
|
requested: Some(rid),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
debug!("Fetch accepted for {rid} from {remote}..");
|
||||||
|
|
||||||
*protocol = Protocol::Fetch;
|
*protocol = Protocol::Fetch;
|
||||||
// Instruct the transport to handover the socket to the worker.
|
// Instruct the transport to handover the socket to the worker.
|
||||||
self.reactor
|
self.reactor
|
||||||
.fetch(*remote, repo, Namespaces::default(), false);
|
.fetch(*remote, rid, Namespaces::default(), true);
|
||||||
}
|
}
|
||||||
(session::State::Connecting { .. }, msg) => {
|
(session::State::Connecting { .. }, msg) => {
|
||||||
error!("Received {:?} from connecting peer {}", msg, peer.id);
|
error!("Received {:?} from connecting peer {}", msg, peer.id);
|
||||||
|
|
|
||||||
|
|
@ -304,8 +304,11 @@ pub enum Message {
|
||||||
zeroes: ZeroBytes,
|
zeroes: ZeroBytes,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Upgrade session to Git protocol and fetch the given repository.
|
/// Request a session upgrade to the Git protocol and fetch the given repository.
|
||||||
Fetch { repo: Id },
|
Fetch { rid: Id },
|
||||||
|
|
||||||
|
/// Accept a fetch request.
|
||||||
|
FetchOk { rid: Id },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
|
|
@ -391,7 +394,8 @@ 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 { repo } => write!(f, "Fetch({repo})"),
|
Self::Fetch { rid } => write!(f, "Fetch({rid})"),
|
||||||
|
Self::FetchOk { rid } => write!(f, "FetchOk({rid})"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use log::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::service::session::Session;
|
use crate::service::session::Session;
|
||||||
use crate::storage::{FetchError, Namespaces, RefUpdate};
|
use crate::storage::Namespaces;
|
||||||
|
|
||||||
use super::message::{Announcement, AnnouncementMessage};
|
use super::message::{Announcement, AnnouncementMessage};
|
||||||
|
|
||||||
|
|
@ -38,16 +38,6 @@ pub struct Fetch {
|
||||||
pub initiated: bool,
|
pub initiated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of a fetch request from a specific seed.
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
pub enum FetchResult {
|
|
||||||
/// Successful fetch from a seed.
|
|
||||||
Fetched { updated: Vec<RefUpdate> },
|
|
||||||
/// Error fetching the resource from a seed.
|
|
||||||
Error { from: NodeId, error: FetchError },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Interface to the network reactor.
|
/// Interface to the network reactor.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Reactor {
|
pub struct Reactor {
|
||||||
|
|
@ -97,11 +87,6 @@ impl Reactor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(&mut self, remote: NodeId, repo: Id, namespaces: Namespaces, initiated: bool) {
|
pub fn fetch(&mut self, remote: NodeId, repo: Id, namespaces: Namespaces, initiated: bool) {
|
||||||
if initiated {
|
|
||||||
debug!("Fetch initiated for {} with {}..", repo, remote);
|
|
||||||
} else {
|
|
||||||
debug!("Fetch requested for {} from {}..", repo, remote);
|
|
||||||
}
|
|
||||||
self.io.push_back(Io::Fetch(Fetch {
|
self.io.push_back(Io::Fetch(Fetch {
|
||||||
repo,
|
repo,
|
||||||
namespaces,
|
namespaces,
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,22 @@ pub enum PingState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Session protocol.
|
/// Session protocol.
|
||||||
#[derive(Debug, Default, Copy, PartialEq, Eq, Clone)]
|
#[derive(Debug, Copy, PartialEq, Eq, Clone)]
|
||||||
pub enum Protocol {
|
pub enum Protocol {
|
||||||
/// The default message-based gossip protocol.
|
/// The default message-based gossip protocol.
|
||||||
#[default]
|
Gossip { requested: Option<Id> },
|
||||||
Gossip,
|
|
||||||
/// Git smart protocol. Used for fetching repository data.
|
/// Git smart protocol. Used for fetching repository data.
|
||||||
/// This protocol is used after a connection upgrade via the
|
/// This protocol is used after a connection upgrade via the
|
||||||
/// [`Message::Fetch`] message.
|
/// [`Message::Fetch`] message.
|
||||||
Fetch,
|
Fetch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Protocol {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Gossip { requested: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
|
|
@ -139,16 +144,15 @@ impl Session {
|
||||||
self.attempts += 1;
|
self.attempts += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch(&mut self, repo: Id) -> Option<Message> {
|
pub fn fetch(&mut self, rid: Id) -> Option<Message> {
|
||||||
if let State::Connected { protocol, .. } = &mut self.state {
|
if let State::Connected { protocol, .. } = &mut self.state {
|
||||||
if let Protocol::Gossip = protocol {
|
if *protocol == (Protocol::Gossip { requested: None }) {
|
||||||
*protocol = Protocol::Fetch;
|
*protocol = Protocol::Gossip {
|
||||||
return Some(Message::Fetch { repo });
|
requested: Some(rid),
|
||||||
|
};
|
||||||
|
return Some(Message::Fetch { rid });
|
||||||
} else {
|
} else {
|
||||||
log::error!(
|
log::error!("Attempted to fetch from peer {} which isn't ready", self.id);
|
||||||
"Attempted to upgrade protocol for {} which was already upgraded",
|
|
||||||
self.id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ use crate::crypto::Signer;
|
||||||
use crate::prelude::Address;
|
use crate::prelude::Address;
|
||||||
use crate::service::reactor::Io;
|
use crate::service::reactor::Io;
|
||||||
use crate::service::{DisconnectReason, Event, Message, NodeId};
|
use crate::service::{DisconnectReason, Event, Message, NodeId};
|
||||||
use crate::storage::WriteStorage;
|
use crate::service::{FetchError, FetchResult};
|
||||||
|
use crate::storage::{WriteRepository, WriteStorage};
|
||||||
use crate::test::peer::Service;
|
use crate::test::peer::Service;
|
||||||
use crate::Link;
|
use crate::Link;
|
||||||
|
|
||||||
|
|
@ -59,6 +60,8 @@ pub enum Input {
|
||||||
Disconnected(NodeId, Rc<DisconnectReason>),
|
Disconnected(NodeId, Rc<DisconnectReason>),
|
||||||
/// 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.
|
||||||
|
Fetched(Arc<FetchResult>),
|
||||||
/// 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,
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +106,13 @@ impl fmt::Display for Scheduled {
|
||||||
Input::Wake => {
|
Input::Wake => {
|
||||||
write!(f, "{}: Tock", self.node)
|
write!(f, "{}: Tock", self.node)
|
||||||
}
|
}
|
||||||
|
Input::Fetched(result) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} <~ {} ({}): FetchCompleted",
|
||||||
|
self.node, result.remote, result.rid
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -399,6 +409,14 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
p.received_message(id, msg);
|
p.received_message(id, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Input::Fetched(result) => {
|
||||||
|
let result = Arc::try_unwrap(result).unwrap();
|
||||||
|
let mut repo = p.storage().repository(result.rid).unwrap();
|
||||||
|
|
||||||
|
repo.fetch(&result.remote, result.namespaces.clone())
|
||||||
|
.unwrap();
|
||||||
|
p.fetched(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for o in p.by_ref() {
|
for o in p.by_ref() {
|
||||||
self.schedule(&node, o);
|
self.schedule(&node, o);
|
||||||
|
|
@ -593,7 +611,37 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
events.push_back(event);
|
events.push_back(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Io::Fetch(..) => todo!("I have no idea what to do here"),
|
Io::Fetch(fetch) => {
|
||||||
|
if self.is_fallible() {
|
||||||
|
self.inbox.insert(
|
||||||
|
self.time + LocalDuration::from_secs(3),
|
||||||
|
Scheduled {
|
||||||
|
node,
|
||||||
|
remote: fetch.remote,
|
||||||
|
input: Input::Fetched(Arc::new(FetchResult {
|
||||||
|
rid: fetch.repo,
|
||||||
|
remote: fetch.remote,
|
||||||
|
namespaces: fetch.namespaces,
|
||||||
|
result: Err(FetchError::Io(io::ErrorKind::Other.into())),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.inbox.insert(
|
||||||
|
self.time + LocalDuration::from_secs(3),
|
||||||
|
Scheduled {
|
||||||
|
node,
|
||||||
|
remote: fetch.remote,
|
||||||
|
input: Input::Fetched(Arc::new(FetchResult {
|
||||||
|
rid: fetch.repo,
|
||||||
|
remote: fetch.remote,
|
||||||
|
namespaces: fetch.namespaces,
|
||||||
|
result: Ok(vec![]),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -868,6 +868,8 @@ fn test_push_and_pull() {
|
||||||
alice.command(service::Command::AnnounceRefs(proj_id));
|
alice.command(service::Command::AnnounceRefs(proj_id));
|
||||||
sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
|
sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
|
||||||
|
|
||||||
|
// TODO: Refs should be compared between the two peers.
|
||||||
|
|
||||||
assert!(eve
|
assert!(eve
|
||||||
.storage()
|
.storage()
|
||||||
.get(&alice.node_id(), proj_id)
|
.get(&alice.node_id(), proj_id)
|
||||||
|
|
@ -880,8 +882,8 @@ fn test_push_and_pull() {
|
||||||
.is_some());
|
.is_some());
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
sim.events(&bob.id).next(),
|
sim.events(&bob.id).next(),
|
||||||
Some(service::Event::RefsFetched { from, .. })
|
Some(service::Event::RefsFetched { remote, .. })
|
||||||
if from == eve.node_id(),
|
if remote == eve.node_id(),
|
||||||
"Bob fetched from Eve"
|
"Bob fetched from Eve"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,8 @@ fn test_replication() {
|
||||||
|
|
||||||
log::debug!(target: "test", "Fetch complete with {}", result.remote);
|
log::debug!(target: "test", "Fetch complete with {}", result.remote);
|
||||||
|
|
||||||
|
// TODO: Have simpler way of listing all refs.
|
||||||
|
|
||||||
let inventory = alice.handle.inventory().unwrap();
|
let inventory = alice.handle.inventory().unwrap();
|
||||||
let alice_refs = alice
|
let alice_refs = alice
|
||||||
.storage
|
.storage
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ pub enum MessageType {
|
||||||
Ping = 10,
|
Ping = 10,
|
||||||
Pong = 12,
|
Pong = 12,
|
||||||
Fetch = 14,
|
Fetch = 14,
|
||||||
|
FetchOk = 16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MessageType> for u16 {
|
impl From<MessageType> for u16 {
|
||||||
|
|
@ -42,6 +43,7 @@ impl TryFrom<u16> for MessageType {
|
||||||
10 => Ok(MessageType::Ping),
|
10 => Ok(MessageType::Ping),
|
||||||
12 => Ok(MessageType::Pong),
|
12 => Ok(MessageType::Pong),
|
||||||
14 => Ok(MessageType::Fetch),
|
14 => Ok(MessageType::Fetch),
|
||||||
|
16 => Ok(MessageType::FetchOk),
|
||||||
_ => Err(other),
|
_ => Err(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +66,7 @@ impl Message {
|
||||||
Self::Ping { .. } => MessageType::Ping,
|
Self::Ping { .. } => MessageType::Ping,
|
||||||
Self::Pong { .. } => MessageType::Pong,
|
Self::Pong { .. } => MessageType::Pong,
|
||||||
Self::Fetch { .. } => MessageType::Fetch,
|
Self::Fetch { .. } => MessageType::Fetch,
|
||||||
|
Self::FetchOk { .. } => MessageType::FetchOk,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
@ -217,8 +220,11 @@ impl wire::Encode for Message {
|
||||||
Self::Pong { zeroes } => {
|
Self::Pong { zeroes } => {
|
||||||
n += zeroes.encode(writer)?;
|
n += zeroes.encode(writer)?;
|
||||||
}
|
}
|
||||||
Self::Fetch { repo } => {
|
Self::Fetch { rid } => {
|
||||||
n += repo.encode(writer)?;
|
n += rid.encode(writer)?;
|
||||||
|
}
|
||||||
|
Self::FetchOk { rid } => {
|
||||||
|
n += rid.encode(writer)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,8 +301,12 @@ impl wire::Decode for Message {
|
||||||
Ok(Self::Pong { zeroes })
|
Ok(Self::Pong { zeroes })
|
||||||
}
|
}
|
||||||
Ok(MessageType::Fetch) => {
|
Ok(MessageType::Fetch) => {
|
||||||
let repo = Id::decode(reader)?;
|
let rid = Id::decode(reader)?;
|
||||||
Ok(Self::Fetch { repo })
|
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)),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,7 @@ where
|
||||||
peer.downgrade();
|
peer.downgrade();
|
||||||
|
|
||||||
self.actions.push_back(Action::RegisterTransport(session));
|
self.actions.push_back(Action::RegisterTransport(session));
|
||||||
self.service.repo_fetched(resp.result);
|
self.service.fetched(resp.result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
|
||||||
let result = FetchResult {
|
let result = FetchResult {
|
||||||
rid: fetch.repo,
|
rid: fetch.repo,
|
||||||
remote: fetch.remote,
|
remote: fetch.remote,
|
||||||
|
namespaces: fetch.namespaces,
|
||||||
result,
|
result,
|
||||||
};
|
};
|
||||||
log::debug!(target: "worker", "Sending response back to service..");
|
log::debug!(target: "worker", "Sending response back to service..");
|
||||||
|
|
@ -125,6 +126,7 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
|
||||||
.arg("--atomic")
|
.arg("--atomic")
|
||||||
.arg("--verbose")
|
.arg("--verbose")
|
||||||
.arg(format!("git://{tunnel_addr}/{}", repo.id))
|
.arg(format!("git://{tunnel_addr}/{}", repo.id))
|
||||||
|
// FIXME: We need to omit our own namespace from this refspec in case we're fetching '*'.
|
||||||
.arg(fetch.namespaces.as_fetchspec())
|
.arg(fetch.namespaces.as_fetchspec())
|
||||||
.stdout(process::Stdio::piped())
|
.stdout(process::Stdio::piped())
|
||||||
.stderr(process::Stdio::piped())
|
.stderr(process::Stdio::piped())
|
||||||
|
|
|
||||||
|
|
@ -631,13 +631,19 @@ impl WriteRepository for Repository {
|
||||||
opts.remote_callbacks(callbacks);
|
opts.remote_callbacks(callbacks);
|
||||||
|
|
||||||
let refspec = if let Some(namespace) = namespace {
|
let refspec = if let Some(namespace) = namespace {
|
||||||
|
// TODO: Make sure we verify before pruning, as pruning may get us into
|
||||||
|
// a state we can't roll back.
|
||||||
|
opts.prune(git2::FetchPrune::On);
|
||||||
|
|
||||||
format!("refs/namespaces/{namespace}/refs/*:refs/namespaces/{namespace}/refs/*")
|
format!("refs/namespaces/{namespace}/refs/*:refs/namespaces/{namespace}/refs/*")
|
||||||
} else {
|
} else {
|
||||||
|
// We should not prune in this case, because it would mean that namespaces that
|
||||||
|
// don't exit on the remote would be deleted locally.
|
||||||
|
opts.prune(git2::FetchPrune::Off);
|
||||||
|
|
||||||
|
// FIXME: We need to omit our own namespace from this refspec.
|
||||||
"refs/namespaces/*:refs/namespaces/*".to_owned()
|
"refs/namespaces/*:refs/namespaces/*".to_owned()
|
||||||
};
|
};
|
||||||
// TODO: Make sure we verify before pruning, as pruning may get us into
|
|
||||||
// a state we can't roll back.
|
|
||||||
opts.prune(git2::FetchPrune::On);
|
|
||||||
// Fetch from the staging copy into the canonical repo.
|
// Fetch from the staging copy into the canonical repo.
|
||||||
remote.fetch(&[refspec], Some(&mut opts), None)?;
|
remote.fetch(&[refspec], Some(&mut opts), None)?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,5 @@
|
||||||
//! Git sub-transport used for fetching radicle data.
|
//! Git sub-transport used for fetching radicle data.
|
||||||
//!
|
|
||||||
//! To have control over the communication, and to allow git streams to be multiplexed over
|
|
||||||
//! existing TCP connections, we implement the [`git2::transport::SmartSubtransport`] trait.
|
|
||||||
//!
|
|
||||||
//! We choose `heartwood` as the URL scheme for this custom transport, and include the node we'd
|
|
||||||
//! like to fetch from, as well as the repository. We expect the TCP stream to already be
|
|
||||||
//! established when this transport is called.
|
|
||||||
//!
|
|
||||||
//! We then maintain a map from node identifier to stream, for all active TCP connections. When a
|
|
||||||
//! URL is requested, we lookup the associated stream and return it to the [`git2`] smart-protocol
|
|
||||||
//! implementation, so that it can carry out the git smart protocol.
|
|
||||||
//!
|
|
||||||
//! This module is meant to be used by registering streams with [`register`].
|
|
||||||
pub mod mock;
|
pub mod mock;
|
||||||
pub mod url;
|
pub mod url;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::sync::Once;
|
|
||||||
|
|
||||||
use git2::transport::SmartSubtransportStream;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
use crate::storage::RemoteId;
|
|
||||||
|
|
||||||
pub use url::{Url, UrlError};
|
pub use url::{Url, UrlError};
|
||||||
|
|
||||||
/// The map of git smart sub-transport streams. We keep a global map because we have
|
|
||||||
/// no control over how [`git2::transport::register`] instantiates our [`Smart`] transport
|
|
||||||
/// or its underlying streams.
|
|
||||||
static STREAMS: Lazy<Mutex<HashMap<RemoteId, Stream>>> = Lazy::new(Default::default);
|
|
||||||
|
|
||||||
/// The stream associated with a repository.
|
|
||||||
type Stream = Box<dyn SmartSubtransportStream>;
|
|
||||||
|
|
||||||
/// Git transport protocol over an I/O stream.
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Smart;
|
|
||||||
|
|
||||||
impl git2::transport::SmartSubtransport for Smart {
|
|
||||||
/// Run a git service on this transport.
|
|
||||||
///
|
|
||||||
/// Based on the URL, which must be a valid [`Url`],
|
|
||||||
/// we retrieve an underlying stream and return it.
|
|
||||||
///
|
|
||||||
/// We only support the upload-pack service, since only fetches are authorized by the
|
|
||||||
/// remote.
|
|
||||||
fn action(
|
|
||||||
&self,
|
|
||||||
url: &str,
|
|
||||||
action: git2::transport::Service,
|
|
||||||
) -> Result<Box<dyn git2::transport::SmartSubtransportStream>, git2::Error> {
|
|
||||||
let url = Url::from_str(url).map_err(|e| git2::Error::from_str(e.to_string().as_str()))?;
|
|
||||||
let mut streams = STREAMS.lock().expect("lock isn't poisoned");
|
|
||||||
|
|
||||||
if let Some(stream) = streams.remove(&url.node) {
|
|
||||||
match action {
|
|
||||||
git2::transport::Service::UploadPackLs | git2::transport::Service::UploadPack => {}
|
|
||||||
git2::transport::Service::ReceivePack | git2::transport::Service::ReceivePackLs => {
|
|
||||||
return Err(git2::Error::from_str(
|
|
||||||
"git-receive-pack is not supported with the custom transport",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(stream)
|
|
||||||
} else {
|
|
||||||
Err(git2::Error::from_str(&format!(
|
|
||||||
"node {} does not have an associated stream",
|
|
||||||
url.node
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close(&self) -> Result<(), git2::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register the radicle transport with `git`.
|
|
||||||
///
|
|
||||||
/// This function can be called more than once. Only one transport will be registered.
|
|
||||||
///
|
|
||||||
pub fn register(node: RemoteId, stream: impl SmartSubtransportStream) {
|
|
||||||
static REGISTER: Once = Once::new();
|
|
||||||
|
|
||||||
// Registration is not thread-safe, so make sure we prevent re-entrancy.
|
|
||||||
REGISTER.call_once(|| unsafe {
|
|
||||||
git2::transport::register(Url::SCHEME, move |remote| {
|
|
||||||
git2::transport::Transport::smart(remote, false, Smart)
|
|
||||||
})
|
|
||||||
.expect("remote transport registration");
|
|
||||||
});
|
|
||||||
|
|
||||||
STREAMS
|
|
||||||
.lock()
|
|
||||||
.expect("lock isn't poisoned")
|
|
||||||
.insert(node, Box::new(stream));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue