A send_timeout that actually times out appears to leave a stale node in the sender wait queue. When the channel is later dropped, List::dequeue finds that node in state 0 rather than WAITING and the debug assertion at src/wait/queue.rs:582 fires.
Reproduction
use std::time::Duration;
use thingbuf::mpsc::blocking;
fn main() {
// ch1: full, receiver alive -> send_timeout gives up after the timeout
let (tx1, _rx1) = blocking::channel::<u64>(1);
tx1.try_send(1).unwrap();
let _ = tx1.send_timeout(2, Duration::from_millis(20));
// ch2: receiver dropped -> send_timeout returns Closed
let (tx2, rx2) = blocking::channel::<u64>(4);
drop(rx2);
let _ = tx2.send_timeout(1, Duration::from_millis(20));
println!("reached end of main");
}
cargo run on 0.1.6:
reached end of main
thread 'main' panicked at .../thingbuf-0.1.6/src/wait/queue.rs:582:9:
assertion `left == right` failed
left: 0
right: 1
Note the panic happens after main's body completes, i.e. during the end-of-scope drops, not inside either send_timeout call. 5 runs out of 5 on my machine.
The assertion
fn dequeue(&mut self, new_state: usize) -> Option<T> {
let mut last = self.tail?;
let last = unsafe { last.as_mut() };
let _prev_state = test_dbg!(last.state.swap(new_state, Release));
debug_assert_eq!(_prev_state, WAITING); // <-- fires, _prev_state == 0
Caveats, in the interest of not wasting your time
- It is timing-sensitive. Inserting
println! calls between the steps, or dropping the handles explicitly in a different order, makes it stop reproducing. Each half in isolation is clean — one channel alone does not trigger it, and neither send_timeout call panics on its own.
debug_assert only. cargo run --release on the same program prints reached end of main and exits 0, so release builds proceed past whatever state the assertion is objecting to. I have not tried to determine whether the resulting list state is merely inconsistent or actually unsafe, and I would not want to guess.
- I have not minimised this further than the above, and I do not know the wait-queue design well enough to propose a fix.
Tested on 0.1.6, Linux x86_64, rustc 1.97.1. Found while benchmarking thingbuf against a ring buffer of my own; the reproduction above is reduced from that test program.
A
send_timeoutthat actually times out appears to leave a stale node in the sender wait queue. When the channel is later dropped,List::dequeuefinds that node in state0rather thanWAITINGand the debug assertion atsrc/wait/queue.rs:582fires.Reproduction
cargo runon 0.1.6:Note the panic happens after
main's body completes, i.e. during the end-of-scope drops, not inside eithersend_timeoutcall. 5 runs out of 5 on my machine.The assertion
Caveats, in the interest of not wasting your time
println!calls between the steps, or dropping the handles explicitly in a different order, makes it stop reproducing. Each half in isolation is clean — one channel alone does not trigger it, and neithersend_timeoutcall panics on its own.debug_assertonly.cargo run --releaseon the same program printsreached end of mainand exits 0, so release builds proceed past whatever state the assertion is objecting to. I have not tried to determine whether the resulting list state is merely inconsistent or actually unsafe, and I would not want to guess.Tested on 0.1.6, Linux x86_64,
rustc 1.97.1. Found while benchmarkingthingbufagainst a ring buffer of my own; the reproduction above is reduced from that test program.