use crate::{ProgressEntry, ProgressListener, Pusher};
use bytes::{Bytes, BytesMut};
#[derive(Debug)]
pub struct BufWriterPusher<P> {
inner: P,
buf: BytesMut,
capacity: usize,
run_start: u64,
}
impl<P: Pusher> BufWriterPusher<P> {
#[must_use]
pub fn new(inner: P, capacity: usize) -> Self {
Self {
inner,
buf: BytesMut::with_capacity(capacity),
capacity,
run_start: 0,
}
}
fn flush_buf(&mut self) -> Result<(), P::Error> {
if self.buf.is_empty() {
return Ok(());
}
let start = self.run_start;
let len = self.buf.len();
let chunk: Bytes = self.buf.split().freeze();
match self.inner.push(&(start..start + len as u64), chunk) {
Ok(()) => Ok(()),
Err((e, rem)) => {
let written = len.saturating_sub(rem.len());
self.buf.extend_from_slice(&rem);
self.run_start = start + written as u64;
Err(e)
}
}
}
}
impl<P: Pusher> Pusher for BufWriterPusher<P> {
type Error = P::Error;
fn set_listener(&mut self, cb: ProgressListener) {
self.inner.set_listener(cb);
}
fn push(&mut self, range: &ProgressEntry, bytes: Bytes) -> Result<(), (Self::Error, Bytes)> {
if bytes.is_empty() {
return Ok(());
}
if !self.buf.is_empty()
&& (range.start != self.run_start + self.buf.len() as u64
|| self.buf.len() + bytes.len() > self.capacity)
&& let Err(e) = self.flush_buf()
{
return Err((e, bytes));
}
if self.buf.is_empty() {
self.run_start = range.start;
}
if bytes.len() >= self.capacity {
return self.inner.push(range, bytes);
}
self.buf.extend_from_slice(bytes.as_ref());
Ok(())
}
fn flush(&mut self) -> Result<(), Self::Error> {
self.flush_buf()?;
self.inner.flush()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use std::sync::{Arc, Mutex};
type PushLog = Arc<Mutex<Vec<(u64, u64, Vec<u8>)>>>;
#[derive(Clone, Debug, Default)]
struct RecordingPusher {
log: PushLog,
}
impl Pusher for RecordingPusher {
type Error = std::io::Error;
fn push(
&mut self,
range: &ProgressEntry,
bytes: Bytes,
) -> Result<(), (Self::Error, Bytes)> {
self.log
.lock()
.unwrap()
.push((range.start, range.end, bytes.to_vec()));
Ok(())
}
}
#[derive(Clone, Debug)]
struct FlakyPusher {
log: PushLog,
did_fail: Arc<std::sync::atomic::AtomicBool>,
}
impl Pusher for FlakyPusher {
type Error = std::io::Error;
fn push(
&mut self,
range: &ProgressEntry,
bytes: Bytes,
) -> Result<(), (Self::Error, Bytes)> {
if self.did_fail.load(std::sync::atomic::Ordering::Relaxed) {
self.log
.lock()
.unwrap()
.push((range.start, range.end, bytes.to_vec()));
return Ok(());
}
self.did_fail
.store(true, std::sync::atomic::Ordering::Relaxed);
Err((std::io::Error::other("boom"), bytes))
}
}
#[derive(Clone, Debug)]
struct PartialPusher {
log: PushLog,
wrote_partial: Arc<std::sync::atomic::AtomicBool>,
}
impl Pusher for PartialPusher {
type Error = std::io::Error;
fn push(
&mut self,
range: &ProgressEntry,
bytes: Bytes,
) -> Result<(), (Self::Error, Bytes)> {
self.log
.lock()
.unwrap()
.push((range.start, range.end, bytes.to_vec()));
if !self
.wrote_partial
.swap(true, std::sync::atomic::Ordering::Relaxed)
{
let rem = bytes.slice(2..);
return Err((std::io::Error::other("partial"), rem));
}
Ok(())
}
}
#[derive(Default)]
struct ListenerRecordingPusher {
fired: Arc<Mutex<Vec<(u64, u64)>>>,
listener: Option<ProgressListener>,
}
impl Pusher for ListenerRecordingPusher {
type Error = std::io::Error;
fn set_listener(&mut self, cb: ProgressListener) {
self.listener = Some(cb);
}
fn push(
&mut self,
range: &ProgressEntry,
_bytes: Bytes,
) -> Result<(), (Self::Error, Bytes)> {
if let Some(cb) = &mut self.listener {
cb(range.clone());
}
self.fired.lock().unwrap().push((range.start, range.end));
Ok(())
}
}
#[test]
fn contiguous_writes_coalesce_into_one_push() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 1024);
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
bp.push(&(4..8), Bytes::from_static(b"efgh")).unwrap();
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 1, "expected a single coalesced push");
assert_eq!(l[0], (0, 8, b"abcdefgh".to_vec()));
drop(l);
}
#[test]
fn noncontiguous_write_flushes_existing_run() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 1024);
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
bp.push(&(10..14), Bytes::from_static(b"efgh")).unwrap();
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 2);
assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
assert_eq!(l[1], (10, 14, b"efgh".to_vec()));
drop(l);
}
#[test]
fn capacity_overflow_flushes() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 4);
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
bp.push(&(4..8), Bytes::from_static(b"efgh")).unwrap();
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 2);
assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
assert_eq!(l[1], (4, 8, b"efgh".to_vec()));
drop(l);
}
#[test]
fn large_write_bypasses_buffer() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 4);
bp.push(&(0..8), Bytes::from_static(b"abcdefgh")).unwrap();
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 1);
assert_eq!(l[0], (0, 8, b"abcdefgh".to_vec()));
drop(l);
}
#[test]
fn random_access_write_is_correct_with_mem_pusher() {
let mem = crate::MemPusher::with_capacity(16);
let mut bp = BufWriterPusher::new(mem, 8 * 1024);
bp.push(&(2..5), Bytes::from_static(b"234")).unwrap();
bp.flush().unwrap();
let content = bp.inner.receive.lock().clone();
assert_eq!(content, b"\0\x00234");
}
#[test]
fn failed_inner_push_is_retained_and_retried() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(
FlakyPusher {
log: log.clone(),
did_fail: Arc::new(std::sync::atomic::AtomicBool::new(false)),
},
1024,
);
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
assert!(bp.flush().is_err());
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 1);
assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
drop(l);
}
#[test]
fn empty_push_is_a_noop() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 1024);
bp.push(&(0..0), Bytes::new()).unwrap();
assert!(log.lock().unwrap().is_empty());
}
#[test]
fn flush_failure_during_push_returns_caller_bytes() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(
FlakyPusher {
log,
did_fail: Arc::new(std::sync::atomic::AtomicBool::new(false)),
},
1024,
);
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
let res = bp.push(&(10..14), Bytes::from_static(b"efgh"));
assert!(res.is_err());
let (_e, remaining) = res.unwrap_err();
assert_eq!(&remaining[..], b"efgh");
}
#[test]
fn flush_buf_partial_write_is_retained_and_retried() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(
PartialPusher {
log: log.clone(),
wrote_partial: Arc::new(std::sync::atomic::AtomicBool::new(false)),
},
1024,
);
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
assert!(bp.flush().is_err());
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 2);
assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
assert_eq!(l[1], (2, 4, b"cd".to_vec()));
}
#[test]
fn buf_writer_forwards_set_listener_and_fires_on_flush() {
let sink = ListenerRecordingPusher::default();
let fired = sink.fired.clone();
let mut bp = BufWriterPusher::new(sink, 1024);
bp.set_listener(Box::new(|_r: ProgressEntry| {}));
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
assert!(
fired.lock().unwrap().is_empty(),
"buffered write must not reach the inner listener before flush"
);
bp.flush().unwrap();
assert_eq!(fired.lock().unwrap().as_slice(), &[(0, 4)]);
}
#[test]
fn capacity_full_does_not_flush_prematurely() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(RecordingPusher { log: log.clone() }, 4);
bp.push(&(0..2), Bytes::from_static(b"ab")).unwrap();
bp.push(&(2..4), Bytes::from_static(b"cd")).unwrap();
assert!(
log.lock().unwrap().is_empty(),
"reaching exactly capacity must not flush"
);
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 1);
assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
}
#[test]
fn failed_noncontiguous_flush_keeps_old_run_for_retry() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut bp = BufWriterPusher::new(
FlakyPusher {
log: log.clone(),
did_fail: Arc::new(std::sync::atomic::AtomicBool::new(false)),
},
1024,
);
bp.push(&(0..4), Bytes::from_static(b"abcd")).unwrap();
let res = bp.push(&(10..14), Bytes::from_static(b"efgh"));
assert!(res.is_err());
bp.flush().unwrap();
let l = log.lock().unwrap();
assert_eq!(l.len(), 1);
assert_eq!(l[0], (0, 4, b"abcd".to_vec()));
}
}