Files
heartwood/radicle-node/src/worker.rs
T
cloudhead 256c620d31 node: Use refs cache to decide on fetching
When deciding whether or not to fetch a repo based on a refs
announcement, we need to know whether our signed refs are stale or not,
compared to the ones in the announcement. This can be quite expensive as
it requires multiple reads from Git's ODB, which often results in
packfile loading.

To remedy this, we introduce a refs cache in our node database that is
consulted when an announcement is received.

We also remove the logic that generates `info` messages, since they are
rarely needed and introduced more complexity.

Additionally, we revert commit 3ad2b4431f,
which introduced logic to minimze wants and haves with additional logic,
since it potentially also increases the load on the ODB.
2024-03-29 16:00:33 +01:00

390 lines
12 KiB
Rust

#![allow(clippy::too_many_arguments)]
mod channels;
mod upload_pack;
pub mod fetch;
pub mod garbage;
use std::path::PathBuf;
use std::{io, time};
use crossbeam_channel as chan;
use radicle::identity::RepoId;
use radicle::node::notifications;
use radicle::prelude::NodeId;
use radicle::storage::refs::RefsAt;
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::{cob, crypto, Storage};
use radicle_fetch::FetchLimit;
use crate::runtime::{thread, Handle};
use crate::service::policy;
use crate::service::policy::Policy;
use crate::wire::StreamId;
pub use channels::{ChannelEvent, Channels};
/// Worker pool configuration.
pub struct Config {
/// Number of worker threads.
pub capacity: usize,
/// Git storage.
pub storage: Storage,
/// Configuration for performing fetched.
pub fetch: FetchConfig,
/// Default policy, if a policy for a specific node or repository was not found.
pub policy: Policy,
/// Default scope, if a scope for a specific repository was not found.
pub scope: policy::Scope,
/// Path to the policies database.
pub policies_db: PathBuf,
}
/// Error returned by fetch.
#[derive(thiserror::Error, Debug)]
pub enum FetchError {
#[error("the 'git fetch' command failed with exit code '{code}'")]
CommandFailed { code: i32 },
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Fetch(#[from] fetch::error::Fetch),
#[error(transparent)]
Handle(#[from] fetch::error::Handle),
#[error(transparent)]
Storage(#[from] radicle::storage::Error),
#[error(transparent)]
PolicyStore(#[from] radicle::node::policy::store::Error),
#[error(transparent)]
Policy(#[from] radicle_fetch::policy::error::Policy),
#[error(transparent)]
Blocked(#[from] radicle_fetch::policy::error::Blocked),
}
impl FetchError {
/// Check if it's a timeout error.
pub fn is_timeout(&self) -> bool {
matches!(self, FetchError::Io(e) if e.kind() == io::ErrorKind::TimedOut)
}
}
/// Error returned by fetch responder.
#[derive(thiserror::Error, Debug)]
pub enum UploadError {
#[error("error parsing git command packet-line: {0}")]
PacketLine(io::Error),
#[error(transparent)]
Io(#[from] io::Error),
#[error("{0} is not authorized to fetch {1}")]
Unauthorized(NodeId, RepoId),
#[error(transparent)]
Storage(#[from] radicle::storage::Error),
#[error(transparent)]
Identity(#[from] radicle::identity::DocError),
#[error(transparent)]
Repository(#[from] radicle::storage::RepositoryError),
#[error(transparent)]
PolicyStore(#[from] radicle::node::policy::store::Error),
}
impl UploadError {
/// Check if it's an end-of-file error.
pub fn is_eof(&self) -> bool {
matches!(self, UploadError::Io(e) if e.kind() == io::ErrorKind::UnexpectedEof)
}
}
/// Fetch job sent to worker thread.
#[derive(Debug, Clone)]
pub enum FetchRequest {
/// Client is initiating a fetch for the repository identified by
/// `rid` from the peer identified by `remote`.
Initiator {
/// Repo to fetch.
rid: RepoId,
/// Remote peer we are interacting with.
remote: NodeId,
/// If this fetch is for a particular set of `rad/sigrefs`.
refs_at: Option<Vec<RefsAt>>,
/// Fetch timeout.
timeout: time::Duration,
},
/// 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 FetchRequest {
pub fn remote(&self) -> NodeId {
match self {
Self::Initiator { remote, .. } | Self::Responder { remote } => *remote,
}
}
}
/// Fetch result of an upload or fetch.
#[derive(Debug)]
pub enum FetchResult {
Initiator {
/// Repo fetched.
rid: RepoId,
/// Fetch result, including remotes fetched.
result: Result<fetch::FetchResult, FetchError>,
},
Responder {
/// Repo requested.
rid: Option<RepoId>,
/// Upload result.
result: Result<(), UploadError>,
},
}
/// Task to be accomplished on a worker thread.
/// This is either going to be an outgoing or incoming fetch.
pub struct Task {
pub fetch: FetchRequest,
pub stream: StreamId,
pub channels: Channels,
}
/// Worker response.
#[derive(Debug)]
pub struct TaskResult {
pub remote: NodeId,
pub result: FetchResult,
pub stream: StreamId,
}
#[derive(Debug, Clone)]
pub struct FetchConfig {
/// Data limits when fetching from a remote.
pub limit: FetchLimit,
/// Public key of the local peer.
pub local: crypto::PublicKey,
/// Configuration for `git gc` garbage collection. Defaults to `1
/// hour ago`.
pub expiry: garbage::Expiry,
}
/// A worker that replicates git objects.
struct Worker {
nid: NodeId,
storage: Storage,
fetch_config: FetchConfig,
tasks: chan::Receiver<Task>,
handle: Handle,
policies: policy::Config<policy::store::Read>,
notifications: notifications::StoreWriter,
cache: cob::cache::StoreWriter,
db: radicle::node::Database,
}
impl Worker {
/// Waits for tasks and runs them. Blocks indefinitely unless there is an error receiving
/// the next task.
fn run(mut self) -> Result<(), chan::RecvError> {
loop {
let task = self.tasks.recv()?;
self.process(task);
}
}
fn process(&mut self, task: Task) {
let Task {
fetch,
channels,
stream,
} = task;
let remote = fetch.remote();
let channels = channels::ChannelsFlush::new(self.handle.clone(), channels, remote, stream);
let result = self._process(fetch, stream, channels, self.notifications.clone());
log::trace!(target: "worker", "Sending response back to service..");
if self
.handle
.worker_result(TaskResult {
remote,
stream,
result,
})
.is_err()
{
log::error!(target: "worker", "Unable to report fetch result: worker channel disconnected");
}
}
fn _process(
&mut self,
fetch: FetchRequest,
stream: StreamId,
mut channels: channels::ChannelsFlush,
notifs: notifications::StoreWriter,
) -> FetchResult {
match fetch {
FetchRequest::Initiator {
rid,
remote,
refs_at,
// TODO: nowhere to use this currently
timeout: _timeout,
} => {
log::debug!(target: "worker", "Worker processing outgoing fetch for {rid}");
let result = self.fetch(rid, remote, refs_at, channels, notifs);
FetchResult::Initiator { rid, result }
}
FetchRequest::Responder { remote } => {
log::debug!(target: "worker", "Worker processing incoming fetch for {remote} on stream {stream}..");
let (mut stream_r, stream_w) = channels.split();
let header = match upload_pack::pktline::git_request(&mut stream_r) {
Ok(header) => header,
Err(e) => {
return FetchResult::Responder {
rid: None,
result: Err(e.into()),
}
}
};
log::debug!(target: "worker", "Spawning upload-pack process for {} on stream {stream}..", header.repo);
if let Err(e) = self.is_authorized(remote, header.repo) {
return FetchResult::Responder {
rid: Some(header.repo),
result: Err(e),
};
}
let result =
upload_pack::upload_pack(&self.nid, &self.storage, &header, stream_r, stream_w)
.map(|_| ())
.map_err(|e| e.into());
log::debug!(target: "worker", "Upload process on stream {stream} exited with result {result:?}");
FetchResult::Responder {
rid: Some(header.repo),
result,
}
}
}
}
fn is_authorized(&self, remote: NodeId, rid: RepoId) -> Result<(), UploadError> {
let policy = self.policies.seed_policy(&rid)?.policy;
let repo = self.storage.repository(rid)?;
let doc = repo.identity_doc()?;
if !doc.is_visible_to(&remote) || policy == Policy::Block {
Err(UploadError::Unauthorized(remote, rid))
} else {
Ok(())
}
}
fn fetch(
&mut self,
rid: RepoId,
remote: NodeId,
refs_at: Option<Vec<RefsAt>>,
channels: channels::ChannelsFlush,
notifs: notifications::StoreWriter,
) -> Result<fetch::FetchResult, FetchError> {
let FetchConfig {
limit,
local,
expiry,
} = &self.fetch_config;
// N.b. if the `rid` is blocked this will return an error, so
// we won't continue with any further set up of the fetch.
let allowed = radicle_fetch::Allowed::from_config(rid, &self.policies)?;
let blocked = radicle_fetch::BlockList::from_config(&self.policies)?;
let mut cache = self.cache.clone();
let handle = fetch::Handle::new(
rid,
*local,
&self.storage,
allowed,
blocked,
channels,
notifs,
)?;
let result = handle.fetch(
rid,
&self.storage,
&mut cache,
&mut self.db,
*limit,
remote,
refs_at,
)?;
if let Err(e) = garbage::collect(&self.storage, rid, *expiry) {
// N.b. ensure that `git gc` works in debug mode.
debug_assert!(false, "`git gc` failed: {e}");
log::warn!(target: "worker", "Failed to run `git gc`: {e}");
}
Ok(result)
}
}
/// A pool of workers. One thread is allocated for each worker.
pub struct Pool {
pool: Vec<thread::JoinHandle<Result<(), chan::RecvError>>>,
}
impl Pool {
/// Create a new worker pool with the given parameters.
pub fn with(
tasks: chan::Receiver<Task>,
nid: NodeId,
handle: Handle,
notifications: notifications::StoreWriter,
cache: cob::cache::StoreWriter,
db: radicle::node::Database,
config: Config,
) -> Result<Self, policy::Error> {
let mut pool = Vec::with_capacity(config.capacity);
for i in 0..config.capacity {
let policies = policy::Config::new(
config.policy,
config.scope,
policy::Store::reader(&config.policies_db)?,
);
let worker = Worker {
nid,
tasks: tasks.clone(),
handle: handle.clone(),
storage: config.storage.clone(),
fetch_config: config.fetch.clone(),
policies,
notifications: notifications.clone(),
cache: cache.clone(),
db: db.clone(),
};
let thread = thread::spawn(&nid, format!("worker#{i}"), || worker.run());
pool.push(thread);
}
Ok(Self { pool })
}
/// Run the worker pool.
///
/// Blocks until all worker threads have exited.
pub fn run(self) -> thread::Result<()> {
for (i, worker) in self.pool.into_iter().enumerate() {
if let Err(err) = worker.join()? {
log::trace!(target: "pool", "Worker {i} exited: {err}");
}
}
log::debug!(target: "pool", "Worker pool shutting down..");
Ok(())
}
}