utaal / spsc-bip-buffer

A concurrent, spsc ring-buffer with sized reservations

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

The possibility of a bug with send-in-drop for BipBufferWriterReservation

Fotosmile opened this issue · comments

Hi! Consider we have the next case:

use std::io;

use spsc_bip_buffer::bip_buffer_with_len;

fn main() {
    let (mut writer, mut reader) = bip_buffer_with_len(16);

    let sender = std::thread::spawn(move || {
        for i in 1..128 {
            let mut reservation = writer.spin_reserve(8);
            if let Err(e) = write_bytes(reservation.as_mut(), i) {
                eprintln!(
                    "WARN: There was the problem ({})... But I must finish my work.",
                    e
                );
            // But the reservation (that contains zeros) will be written in the drop anyway!
            } else {
                reservation.send();
            }
        }
    });
    let receiver = std::thread::spawn(move || {
        for i in (1 ..128).step_by(2) {
            while reader.valid().len() < 8 {}
            assert_eq!(&reader.valid()[..8], &[10, 11, 12, 13, 14, 15, 16, i]); // sometimes there will be zeros!
            reader.consume(8);
        }
    });
    sender.join().unwrap();
    receiver.join().unwrap();
}

fn write_bytes(mut stream: impl io::Write, i: u8) -> io::Result<()> {
    if i % 2 == 0 {
        Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "This case can happen. The error can be produced at any moment.",
        ))
    } else {
        stream.write_all(&[10, 11, 12, 13, 14, 15, 16, i])
    }
}

(the error is not explicit, and users can spend some time to figure out where is the problem)
So, the reservation does not always have to be sent to the receiver, as there are cases when something went wrong with the reservation (for example, an error occurred and you need to try to reserve and fill the buffer later). Therefore, I think the code from drop should be moved to send_impl.
But, of course, we should not leave users without auto-sending, as not everyone has cases where errors are possible. Therefore, I think we should move the code from drop for BipBufferWriterReservation to the send_impl method, and delete the drop method. But add another struct BipBufferWriterReservationAutoSend(BipBufferWriterReservation) (the name should be better), which can be obtained from the call for the BipBufferWriter::reserve_auto_send method, and BipBufferWriterReservationAutoSend can look like this:

impl<'a> BipBufferWriterReservation<'a> {
    /// Calling `send` (or simply dropping the reservation) marks the end of the write and informs
    /// the reader that the data in this slice can now be read.
    pub fn send(mut self) {
        self.send_impl()
    }

    pub fn send_impl(&mut self) {
        if self.wraparound {
            self.writer
                .buffer
                .last
                .0
                .store(self.writer.write, Ordering::Relaxed);
            self.writer.write = 0;
        }
        self.writer.write += self.len;
        if self.writer.write > self.writer.last {
            self.writer.last = self.writer.write;
            self.writer
                .buffer
                .last
                .0
                .store(self.writer.last, Ordering::Relaxed);
        }
        self.writer
            .buffer
            .write
            .0
            .store(self.writer.write, Ordering::Release);

        #[cfg(feature = "debug")]
        eprintln!("+++{}", self.writer.buffer.dbg_info());
    }
}

// The Drop impl for BipBufferWriterReservation is removed

pub struct BipBufferWriterReservationAutoSend<'a>(BipBufferWriterReservation<'a>);

impl<'a> core::ops::Deref for BipBufferWriterReservationAutoSend<'a> {
    type Target = BipBufferWriterReservation<'a>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<'a> core::ops::DerefMut for BipBufferWriterReservationAutoSend<'a> {
    fn deref_mut(&mut self) -> &mut BipBufferWriterReservation<'a> {
        &mut self.0
    }
}

impl<'a> core::ops::Drop for BipBufferWriterReservationAutoSend<'a> {
    fn drop(&mut self) {
        self.send_impl();
    }
}

Hi! Thanks for the detailed write-up! You’re totally right we should have a way to cancel the reservation. What about we leverage truncate for this as well? We could have an additional .cancel method that zeroes the length of the reservation, which effectively cancels the send operation.

Yeah, the cancel can be a solution for some cases. But there may be the case when you must manually cancel a reservation in all Result cases:

struct WriterFuture {
    writer: BipBufferWriter,
    stream: TcpStream,
}

impl Stream for WriterFuture {
    type Item = usize;
    type Error = io::Error;

    fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
        loop {
            if let Some(mut reserved) = self.writer.reserve(1024) {
                match self.stream.poll_read(reserved.as_mut()) {
                    Ok(Async::Ready(Some(0))) => {
                        reserved.cancel(); // first cancel

                        return Ok(Async::Ready(None));
                    }
                    Ok(Async::Ready(Some(n))) => {
                        // do something with the buffer
                        reserved.truncate(n);
                    }
                    Ok(Async::Ready(None)) => {
                        reserved.cancel(); // second cancel

                        return Ok(Async::Ready(None));
                    }
                    Ok(Async::NotReady) => {
                        reserved.cancel(); // third cancel

                        return Ok(Async::NotReady);
                    }
                    Err(e) => {
                        reserved.cancel(); // fourth cancel

                        return Err(e);
                    }
                }
            } else {
                return Ok(Async::NotReady);
            }
        }
    }
}

So, it is not very comfortable to cancel the reservation for every case. Instead, I would like to use BipBufferWriterReservation without auto-send-in-drop implementation. Then, the code will look like this:

struct WriterFuture {
    writer: BipBufferWriter,
    stream: TcpStream,
}

impl Stream for WriterFuture {
    type Item = usize;
    type Error = io::Error;

    fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
        let mut bytes_written = 0;
        loop {
            if let Some(mut reserved) = self.writer.reserve(1024) {
                match try_ready!(self.stream.poll_read(reserved.as_mut())) {
                    Some(0) => {
                        return Ok(Async::Ready(None));
                    }
                    Some(n) => {
                        // do something with the buffer
                        reserved.truncate(n);

                        reserved.send(); // explicit send, but only one!
                    }
                    None => {
                        return Ok(Async::Ready(None));
                    }
                }
            } else {
                return Ok(Async::NotReady);
            }
        }
    }
}

So, without send-in-drop we are able to not handle every possible Result with sub-options to manually cancel the reservation. Instead, we call only once send. And in terms of optimization, with send-in-drop, the sending of bytes (even if the cancel method was called) will be called every time what processor time will be spent on, but what will not be without send-in-drop. Without send-in-drop, the code to send bytes is executed only for the real sending of bytes.

That's a good point! Thank you for the detailed explanation and code examples.
I've updated the pull request with a new method, set_manual_send: when called, it disables the auto-send behaviour, and requires an explicit send. I think your code example would look like the following:

struct WriterFuture {
    writer: BipBufferWriter,
    stream: TcpStream,
}

impl Stream for WriterFuture {
    type Item = usize;
    type Error = io::Error;

    fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
        let mut bytes_written = 0;
        loop {
            if let Some(mut reserved) = self.writer.reserve(1024) {
                reserved.set_manual_send();
                match try_ready!(self.stream.poll_read(reserved.as_mut())) {
                    Some(0) => {
                        return Ok(Async::Ready(None));
                    }
                    Some(n) => {
                        // do something with the buffer
                        reserved.truncate(n);

                        reserved.send(); // explicit send, but only one!
                    }
                    None => {
                        return Ok(Async::Ready(None));
                    }
                }
            } else {
                return Ok(Async::NotReady);
            }
        }
    }
}

Thoughts?

set_manual_send looks good. Thank you for the code updates! :)