Make extraction cancellable for bin and tar based formats (#481)

Extraction wasn't cancellable by `cancel_on_user_sig_term` used in `entry` since it calls `block_in_place`.

This PR adds cancellation support to it by adding a `static` variable `OnceCell` to `wait_on_cancellation_signal` so that once it returns `Ok(())`, all other calls to it after that point also returns `Ok(())` immediately.

`StreamReadable`, which is used in cancellation process, then stores a boxed future of `wait_on_cancellation_signal` and polled it in `BufReader::fill_buf`.

Note that for zip, the extraction process takes `File` instead of `StreamReadable` due to `io::Seek` requirement, so it cancelling during extraction for zip is still not possible.

This PR also optimized `extract_bin` and `extract_zip` by using `StreamReadable::copy` introduced to this PR instead of `io::copy`, which allocates an internal buffer on stack, which imposes extra copy.

It also fixed `StreamReadable::fill_buf` by ensuring that empty buffer is only returned on eof.

* Make `wait_on_cancellation_signal` pub
* Enable feature `parking_lot` of dep tokio
* Mod `wait_on_cancellation_signal`: Use `OnceCell` internally
   to archive the effect that once call to it return `Ok(())`, all calls to
   it after that also returns `Ok(())`.
* Impl `From<BinstallError>` for `io::Error`
* Impl cancellation on user signal in `StreamReadable`
* Fix err msg when cancelling during extraction in `ops::resolve`
* Optimize: Impl & use `StreamReadable::copy`
   which is same as `io::copy` but does not allocate any internal buffer
   since `StreamReadable` is buffered.
* Fix `next_stream`: Return non-empty bytes on `Ok(Some(bytes))`

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
Jiahao XU 2022-10-13 11:31:13 +11:00 committed by GitHub
parent fdc617d870
commit aa6012baae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 109 additions and 20 deletions

View file

@ -1,24 +1,26 @@
use std::{
cmp::min,
io::{self, BufRead, Read},
future::Future,
io::{self, BufRead, Read, Write},
pin::Pin,
};
use bytes::{Buf, Bytes};
use futures_util::stream::{Stream, StreamExt};
use tokio::runtime::Handle;
use crate::errors::BinstallError;
use crate::{errors::BinstallError, helpers::signal::wait_on_cancellation_signal};
/// This wraps an AsyncIterator as a `Read`able.
/// It must be used in non-async context only,
/// meaning you have to use it with
/// `tokio::task::{block_in_place, spawn_blocking}` or
/// `std::thread::spawn`.
#[derive(Debug)]
pub struct StreamReadable<S> {
stream: S,
handle: Handle,
bytes: Bytes,
cancellation_future: Pin<Box<dyn Future<Output = Result<(), io::Error>> + Send>>,
}
impl<S> StreamReadable<S> {
@ -27,6 +29,39 @@ impl<S> StreamReadable<S> {
stream,
handle: Handle::current(),
bytes: Bytes::new(),
cancellation_future: Box::pin(wait_on_cancellation_signal()),
}
}
}
impl<S, E> StreamReadable<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
BinstallError: From<E>,
{
/// Copies from `self` to `writer`.
///
/// Same as `io::copy` but does not allocate any internal buffer
/// since `self` is buffered.
pub(super) fn copy<W>(&mut self, mut writer: W) -> io::Result<()>
where
W: Write,
{
self.copy_inner(&mut writer)
}
fn copy_inner(&mut self, writer: &mut dyn Write) -> io::Result<()> {
loop {
let buf = self.fill_buf()?;
if buf.is_empty() {
// Eof
break Ok(());
}
writer.write_all(buf)?;
let n = buf.len();
self.consume(n);
}
}
}
@ -56,6 +91,27 @@ where
Ok(n)
}
}
/// If `Ok(Some(bytes))` if returned, then `bytes.is_empty() == false`.
async fn next_stream<S, E>(stream: &mut S) -> io::Result<Option<Bytes>>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
BinstallError: From<E>,
{
loop {
let option = stream
.next()
.await
.transpose()
.map_err(BinstallError::from)?;
match option {
Some(bytes) if bytes.is_empty() => continue,
option => break Ok(option),
}
}
}
impl<S, E> BufRead for StreamReadable<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
@ -65,13 +121,18 @@ where
let bytes = &mut self.bytes;
if !bytes.has_remaining() {
match self.handle.block_on(async { self.stream.next().await }) {
Some(Ok(new_bytes)) => *bytes = new_bytes,
Some(Err(e)) => {
let e: BinstallError = e.into();
return Err(io::Error::new(io::ErrorKind::Other, e));
let option = self.handle.block_on(async {
tokio::select! {
res = next_stream(&mut self.stream) => res,
res = self.cancellation_future.as_mut() => {
Err(res.err().unwrap_or_else(|| io::Error::from(BinstallError::UserAbort)))
},
}
None => (),
})?;
if let Some(new_bytes) = option {
// new_bytes are guaranteed to be non-empty.
*bytes = new_bytes;
}
}
Ok(&*bytes)