node: report upload-pack progress
Emit the `UploadPack` events from the `upload_pack` method. A wrapper type, `Reporter`, is introduced for implementing a `Write` instance that will parse the given bytes as `gix_protocol::RemoteProgress`, if possible, and emit it via the `Emitter`, and then write the bytes to the underlying writer. This allows any subscribers to see the progress of an upload-pack, i.e. `Compressing objects`, `Counting object`, `Enumerating objects`. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
parent
89627a8b6a
commit
c8a24d5584
|
|
@ -2537,6 +2537,7 @@ dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"cyphernet",
|
"cyphernet",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"gix-protocol",
|
||||||
"io-reactor",
|
"io-reactor",
|
||||||
"lexopt",
|
"lexopt",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ colored = { version = "2.1.0" }
|
||||||
crossbeam-channel = { version = "0.5.6" }
|
crossbeam-channel = { version = "0.5.6" }
|
||||||
cyphernet = { version = "0.5.0", features = ["tor", "dns", "ed25519", "p2p-ed25519"] }
|
cyphernet = { version = "0.5.0", features = ["tor", "dns", "ed25519", "p2p-ed25519"] }
|
||||||
fastrand = { version = "2.0.0" }
|
fastrand = { version = "2.0.0" }
|
||||||
|
gix-protocol = { version = "0.41.1", features = ["blocking-client"] }
|
||||||
io-reactor = { version = "0.5.1", features = ["popol"] }
|
io-reactor = { version = "0.5.1", features = ["popol"] }
|
||||||
lexopt = { version = "0.3.0" }
|
lexopt = { version = "0.3.0" }
|
||||||
libc = { version = "0.2.137" }
|
libc = { version = "0.2.137" }
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,10 @@ where
|
||||||
pub fn local_time(&self) -> LocalTime {
|
pub fn local_time(&self) -> LocalTime {
|
||||||
self.clock
|
self.clock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn emitter(&self) -> Emitter<Event> {
|
||||||
|
self.emitter.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D, S, G> Service<D, S, G>
|
impl<D, S, G> Service<D, S, G>
|
||||||
|
|
|
||||||
|
|
@ -721,7 +721,10 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
let task = Task {
|
let task = Task {
|
||||||
fetch: FetchRequest::Responder { remote: *nid },
|
fetch: FetchRequest::Responder {
|
||||||
|
remote: *nid,
|
||||||
|
emitter: self.service.emitter(),
|
||||||
|
},
|
||||||
stream,
|
stream,
|
||||||
channels,
|
channels,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ use std::path::PathBuf;
|
||||||
use crossbeam_channel as chan;
|
use crossbeam_channel as chan;
|
||||||
|
|
||||||
use radicle::identity::RepoId;
|
use radicle::identity::RepoId;
|
||||||
use radicle::node::notifications;
|
use radicle::node::{notifications, Event};
|
||||||
use radicle::prelude::NodeId;
|
use radicle::prelude::NodeId;
|
||||||
use radicle::storage::refs::RefsAt;
|
use radicle::storage::refs::RefsAt;
|
||||||
use radicle::storage::{ReadRepository, ReadStorage};
|
use radicle::storage::{ReadRepository, ReadStorage};
|
||||||
use radicle::{cob, crypto, Storage};
|
use radicle::{cob, crypto, Storage};
|
||||||
use radicle_fetch::FetchLimit;
|
use radicle_fetch::FetchLimit;
|
||||||
|
|
||||||
use crate::runtime::{thread, Handle};
|
use crate::runtime::{thread, Emitter, Handle};
|
||||||
use crate::service::policy;
|
use crate::service::policy;
|
||||||
use crate::service::policy::Policy;
|
use crate::service::policy::Policy;
|
||||||
use crate::wire::StreamId;
|
use crate::wire::StreamId;
|
||||||
|
|
@ -113,13 +113,15 @@ pub enum FetchRequest {
|
||||||
Responder {
|
Responder {
|
||||||
/// Remote peer we are interacting with.
|
/// Remote peer we are interacting with.
|
||||||
remote: NodeId,
|
remote: NodeId,
|
||||||
|
/// Reporter for upload-pack progress.
|
||||||
|
emitter: Emitter<Event>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FetchRequest {
|
impl FetchRequest {
|
||||||
pub fn remote(&self) -> NodeId {
|
pub fn remote(&self) -> NodeId {
|
||||||
match self {
|
match self {
|
||||||
Self::Initiator { remote, .. } | Self::Responder { remote } => *remote,
|
Self::Initiator { remote, .. } | Self::Responder { remote, .. } => *remote,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +235,7 @@ impl Worker {
|
||||||
let result = self.fetch(rid, remote, refs_at, channels, notifs);
|
let result = self.fetch(rid, remote, refs_at, channels, notifs);
|
||||||
FetchResult::Initiator { rid, result }
|
FetchResult::Initiator { rid, result }
|
||||||
}
|
}
|
||||||
FetchRequest::Responder { remote } => {
|
FetchRequest::Responder { remote, emitter } => {
|
||||||
log::debug!(target: "worker", "Worker processing incoming fetch for {remote} on stream {stream}..");
|
log::debug!(target: "worker", "Worker processing incoming fetch for {remote} on stream {stream}..");
|
||||||
|
|
||||||
let (mut stream_r, stream_w) = channels.split();
|
let (mut stream_r, stream_w) = channels.split();
|
||||||
|
|
@ -255,8 +257,15 @@ impl Worker {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let result =
|
let result = upload_pack::upload_pack(
|
||||||
upload_pack::upload_pack(&self.nid, &self.storage, &header, stream_r, stream_w)
|
&self.nid,
|
||||||
|
remote,
|
||||||
|
&self.storage,
|
||||||
|
&emitter,
|
||||||
|
&header,
|
||||||
|
stream_r,
|
||||||
|
stream_w,
|
||||||
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|e| e.into());
|
.map_err(|e| e.into());
|
||||||
log::debug!(target: "worker", "Upload process on stream {stream} exited with result {result:?}");
|
log::debug!(target: "worker", "Upload process on stream {stream} exited with result {result:?}");
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::{Command, ExitStatus, Stdio};
|
use std::process::{Command, ExitStatus, Stdio};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use radicle::node::NodeId;
|
use gix_protocol::transport::bstr::ByteSlice;
|
||||||
|
use radicle::identity::RepoId;
|
||||||
|
use radicle::node::events;
|
||||||
|
use radicle::node::events::Emitter;
|
||||||
|
use radicle::node::{Event, NodeId};
|
||||||
use radicle::storage::git::paths;
|
use radicle::storage::git::paths;
|
||||||
use radicle::Storage;
|
use radicle::Storage;
|
||||||
|
|
||||||
|
|
@ -16,15 +21,18 @@ use crate::runtime::thread;
|
||||||
/// send the EOF file message.
|
/// send the EOF file message.
|
||||||
pub fn upload_pack<R, W>(
|
pub fn upload_pack<R, W>(
|
||||||
nid: &NodeId,
|
nid: &NodeId,
|
||||||
|
remote: NodeId,
|
||||||
storage: &Storage,
|
storage: &Storage,
|
||||||
|
emitter: &Emitter<Event>,
|
||||||
header: &pktline::GitRequest,
|
header: &pktline::GitRequest,
|
||||||
mut recv: R,
|
mut recv: R,
|
||||||
mut send: W,
|
send: W,
|
||||||
) -> io::Result<ExitStatus>
|
) -> io::Result<ExitStatus>
|
||||||
where
|
where
|
||||||
R: io::Read + Send,
|
R: io::Read + Send,
|
||||||
W: io::Write + Send,
|
W: io::Write + Send,
|
||||||
{
|
{
|
||||||
|
let timer = Instant::now();
|
||||||
let protocol_version = header
|
let protocol_version = header
|
||||||
.extra
|
.extra
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -75,14 +83,21 @@ where
|
||||||
|
|
||||||
let mut stdin = child.stdin.take().unwrap();
|
let mut stdin = child.stdin.take().unwrap();
|
||||||
let mut stdout = io::BufReader::new(child.stdout.take().unwrap());
|
let mut stdout = io::BufReader::new(child.stdout.take().unwrap());
|
||||||
|
let mut reporter = Reporter {
|
||||||
|
rid: header.repo,
|
||||||
|
remote,
|
||||||
|
emitter: emitter.clone(),
|
||||||
|
send,
|
||||||
|
};
|
||||||
thread::scope(|s| {
|
thread::scope(|s| {
|
||||||
thread::spawn_scoped(nid, "upload-pack", s, || {
|
thread::spawn_scoped(nid, "upload-pack", s, || {
|
||||||
// N.b. we indefinitely copy stdout to the sender,
|
// N.b. we indefinitely copy stdout to the sender,
|
||||||
// i.e. there's no need for a loop.
|
// i.e. there's no need for a loop.
|
||||||
match io::copy(&mut stdout, &mut send) {
|
match io::copy(&mut stdout, &mut reporter) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "worker", "Worker channel disconnected for {}; aborting: {e}", header.repo);
|
log::error!(target: "worker", "Worker channel disconnected for {}; aborting: {e}", header.repo);
|
||||||
|
emitter.emit(events::UploadPack::error(header.repo, remote, e).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -104,6 +119,7 @@ where
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(target: "worker", "Error on upload-pack channel read for {}: {e}", header.repo);
|
log::error!(target: "worker", "Error on upload-pack channel read for {}: {e}", header.repo);
|
||||||
|
emitter.emit(events::UploadPack::error(header.repo, remote, e).into());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,9 +140,62 @@ where
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let status = child.wait()?;
|
let status = child.wait()?;
|
||||||
|
emitter.emit(events::UploadPack::done(header.repo, remote, status).into());
|
||||||
|
log::debug!(target: "worker", "Upload pack finished ({}ms)", timer.elapsed().as_millis());
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A combination of the upload-pack sender with an [`Emitter`] for reporting
|
||||||
|
/// the progress events to subscribers.
|
||||||
|
struct Reporter<W> {
|
||||||
|
rid: RepoId,
|
||||||
|
remote: NodeId,
|
||||||
|
emitter: Emitter<Event>,
|
||||||
|
send: W,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W> Reporter<W> {
|
||||||
|
fn emit(&self, buf: &[u8]) {
|
||||||
|
if let Some(progress) = Self::as_upload_pack_progress(buf) {
|
||||||
|
log::trace!(target: "worker", "upload-pack progress: {progress}");
|
||||||
|
self.emitter
|
||||||
|
.emit(events::UploadPack::write(self.rid, self.remote, progress).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_upload_pack_progress(buf: &[u8]) -> Option<events::upload_pack::Progress> {
|
||||||
|
use events::upload_pack::Progress::*;
|
||||||
|
let gix_protocol::RemoteProgress {
|
||||||
|
action, step, max, ..
|
||||||
|
} = gix_protocol::RemoteProgress::from_bytes(buf)?;
|
||||||
|
if action.contains_str("Counting objects") {
|
||||||
|
step.and_then(|processed| max.map(|total| Counting { processed, total }))
|
||||||
|
} else if action.contains_str("Compressing objects") {
|
||||||
|
step.and_then(|processed| max.map(|total| Compressing { processed, total }))
|
||||||
|
} else if action.contains_str("Enumerating objects") {
|
||||||
|
max.map(|total| Enumerating { total })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W> io::Write for Reporter<W>
|
||||||
|
where
|
||||||
|
W: io::Write,
|
||||||
|
{
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let n = self.send.write(buf)?;
|
||||||
|
self.send.flush()?;
|
||||||
|
self.emit(buf);
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.send.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) mod pktline {
|
pub(super) mod pktline {
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue