Skip to main content

vane_core/engine/
mio_engine.rs

1//! mio (epoll/kqueue) fallback backend — completion semantics over readiness
2//! events (`IO-05`).
3//!
4//! Each descriptor is registered **once** (read+write interest). Pending
5//! operations are queued per fd; on readiness the syscalls run inline
6//! against the worker's stable-address buffer pool (the same slots the
7//! io_uring backend registers as fixed buffers) and synthetic CQEs are
8//! emitted. Upper layers cannot distinguish the two engines.
9
10use std::collections::HashMap;
11use std::io;
12use std::net::SocketAddr;
13use std::os::fd::{IntoRawFd, RawFd};
14use std::path::Path;
15use std::time::Duration;
16
17use mio::{Events, Interest, Token as MioToken, Waker};
18
19use super::{Cqe, Engine, Poll};
20use crate::buffer::BufferPool;
21use crate::token::Token;
22
23/// Internal event capacity.
24const EVENTS_CAP: usize = 1024;
25
26/// Waker token (shutdown signal from another thread).
27const WAKER: usize = u64::MAX as usize;
28
29/// A queued operation waiting for readiness. Keyed by fd, dispatched on
30/// matching readiness.
31enum Pending {
32    /// Socket read into `slot`.
33    Read { slot: u32, token: Token },
34    /// Write `slot[offset..len]`.
35    Write {
36        slot: u32,
37        len: usize,
38        offset: usize,
39        token: Token,
40    },
41    /// Nonblocking connect in progress.
42    Connect { token: Token },
43    /// One direction of an L4 splice pump: `from -> to`.
44    Splice {
45        from: RawFd,
46        to: RawFd,
47        token: Token,
48    },
49    /// Listener accept readiness.
50    Listener { lfd: RawFd, token: Token },
51}
52
53/// mio-backed [`Engine`].
54pub struct MioEngine {
55    poller: mio::Poll,
56    #[allow(dead_code)] // future cross-thread wake (admin triggers)
57    waker: Waker,
58    /// Slot base pointers (stable for the pool's lifetime).
59    slot_bases: Vec<*mut u8>,
60    buf_size: usize,
61    /// RawFd -> armed ops.
62    pending: HashMap<RawFd, Vec<Pending>>,
63    events: Events,
64    cqes: Vec<Cqe>,
65}
66
67// SAFETY: slot base pointers point into the worker-owned BufferPool; the
68// engine lives and dies on the worker thread which outlives the pool usage.
69unsafe impl Send for MioEngine {}
70
71impl MioEngine {
72    /// Creates the engine bound to the worker's buffer pool.
73    ///
74    /// # Errors
75    /// Poller creation failure.
76    pub fn new(pool: &BufferPool) -> io::Result<Self> {
77        let poller = mio::Poll::new()?;
78        let waker = Waker::new(poller.registry(), MioToken(WAKER))?;
79        // Base addresses only (no dereference here); slots stay valid for
80        // the engine's lifetime (worker owns both, same thread).
81        let slot_bases = {
82            (0..pool.capacity() as u32)
83                .map(|i| pool.slot(i).as_ptr() as *mut u8)
84                .collect::<Vec<_>>()
85        };
86        Ok(Self {
87            poller,
88            waker,
89            slot_bases,
90            buf_size: pool.buf_size(),
91            pending: HashMap::new(),
92            events: Events::with_capacity(EVENTS_CAP),
93            cqes: Vec::with_capacity(EVENTS_CAP),
94        })
95    }
96
97    fn slot_ptr(&self, slot: u32) -> *mut u8 {
98        self.slot_bases[slot as usize]
99    }
100
101    fn register(&mut self, fd: RawFd) -> io::Result<()> {
102        let mut src = mio::unix::SourceFd(&fd);
103        self.poller.registry().register(
104            &mut src,
105            MioToken(fd as usize),
106            Interest::READABLE.add(Interest::WRITABLE),
107        )
108    }
109
110    fn push(&mut self, fd: RawFd, op: Pending) {
111        self.pending.entry(fd).or_default().push(op);
112    }
113
114    /// Performs one read attempt: full/EOF/error results queue a CQE
115    /// (true); WouldBlock parks the op for the readiness edge (false).
116    fn try_read_now(&mut self, fd: RawFd, slot: u32, token: &Token) -> bool {
117        let ptr = self.slot_ptr(slot);
118        // SAFETY: slot exclusively ours while its read is in flight; the
119        // worker does not touch it until the CQE lands.
120        let res = unsafe { libc::read(fd, ptr.cast(), self.buf_size) };
121        if res >= 0 {
122            crate::dbg_trace!(
123                "RDNOW fd={fd} n={res}{}",
124                if res == 0 { " (EOF?)" } else { "" }
125            );
126            self.cqes.push(Cqe {
127                token: *token,
128                result: Ok(res as u32),
129            });
130            true
131        } else {
132            let err = io::Error::last_os_error();
133            if err.kind() == io::ErrorKind::WouldBlock {
134                crate::dbg_trace!("RDPARK fd={fd}");
135                self.push(
136                    fd,
137                    Pending::Read {
138                        slot,
139                        token: *token,
140                    },
141                );
142                false
143            } else {
144                self.cqes.push(Cqe {
145                    token: *token,
146                    result: Err(err),
147                });
148                true
149            }
150        }
151    }
152
153    fn finish_read(&mut self, fd: RawFd, slot: u32, token: Token) {
154        // Readiness-driven retry: same semantics as the inline attempt.
155        let _ = self.try_read_now(fd, slot, &token);
156    }
157
158    /// Performs one write attempt: full success queues a CQE (true);
159    /// partial/WouldBlock parks the remainder for the edge (false); fatal
160    /// errors queue an error CQE (true).
161    fn try_write_now(
162        &mut self,
163        fd: RawFd,
164        slot: u32,
165        len: usize,
166        offset: usize,
167        token: &Token,
168    ) -> bool {
169        let ptr = self.slot_ptr(slot);
170        // SAFETY: slot bytes serialized by the session pre-submit.
171        let res = unsafe { libc::write(fd, ptr.add(offset).cast(), len - offset) };
172        if res >= 0 {
173            let n = res as usize;
174            if offset + n >= len {
175                self.cqes.push(Cqe {
176                    token: *token,
177                    result: Ok(len as u32),
178                });
179                true
180            } else {
181                self.push(
182                    fd,
183                    Pending::Write {
184                        slot,
185                        len,
186                        offset: offset + n,
187                        token: *token,
188                    },
189                );
190                false
191            }
192        } else {
193            let err = io::Error::last_os_error();
194            if err.kind() == io::ErrorKind::WouldBlock {
195                self.push(
196                    fd,
197                    Pending::Write {
198                        slot,
199                        len,
200                        offset,
201                        token: *token,
202                    },
203                );
204                false
205            } else {
206                self.cqes.push(Cqe {
207                    token: *token,
208                    result: Err(err),
209                });
210                true
211            }
212        }
213    }
214
215    fn finish_write(&mut self, fd: RawFd, slot: u32, len: usize, offset: usize, token: Token) {
216        // Readiness-driven retry: same semantics as the inline attempt.
217        let _ = self.try_write_now(fd, slot, len, offset, &token);
218        let _ = fd;
219    }
220
221    fn pump_splice(&mut self, from: RawFd, to: RawFd, token: Token) {
222        match crate::splice::pump(from, to, 1 << 20) {
223            crate::splice::PumpResult::Moved(n) => {
224                self.cqes.push(Cqe {
225                    token,
226                    result: Ok(n as u32),
227                });
228            }
229            crate::splice::PumpResult::Eof => {
230                self.cqes.push(Cqe {
231                    token,
232                    result: Ok(0),
233                });
234            }
235            crate::splice::PumpResult::WouldBlock => { /* rearm below */ }
236            crate::splice::PumpResult::Err(code) => {
237                self.cqes.push(Cqe {
238                    token,
239                    result: Err(io::Error::from_raw_os_error(code)),
240                });
241                return;
242            }
243        }
244        // Stay armed: readiness refires if data remains.
245        self.push(from, Pending::Splice { from, to, token });
246    }
247
248    /// Runs all armed ops matching an event.
249    fn dispatch(&mut self, fd: RawFd, readable: bool, writable: bool) {
250        let ops = self.pending.remove(&fd).unwrap_or_default();
251        // Priority: connect completion first, then listener/read/write.
252        for op in ops {
253            match op {
254                Pending::Connect { token } => {
255                    let err = sock_error(fd);
256                    self.cqes.push(Cqe {
257                        token,
258                        result: if err == 0 {
259                            Ok(0)
260                        } else {
261                            Err(io::Error::from_raw_os_error(err))
262                        },
263                    });
264                }
265                Pending::Listener { lfd, token } => {
266                    let _ = lfd;
267                    if readable {
268                        self.cqes.push(Cqe {
269                            token,
270                            result: Ok(0),
271                        });
272                    } else {
273                        // Keep armed.
274                        // (lfd may differ from the event fd only in tests.)
275                        self.push(fd, Pending::Listener { lfd, token });
276                    }
277                }
278                Pending::Read { slot, token } => {
279                    if readable {
280                        self.finish_read(fd, slot, token);
281                    } else {
282                        self.push(fd, Pending::Read { slot, token });
283                    }
284                }
285                Pending::Write {
286                    slot,
287                    len,
288                    offset,
289                    token,
290                } => {
291                    if writable {
292                        self.finish_write(fd, slot, len, offset, token);
293                    } else {
294                        self.push(
295                            fd,
296                            Pending::Write {
297                                slot,
298                                len,
299                                offset,
300                                token,
301                            },
302                        );
303                    }
304                }
305                Pending::Splice { from, to, token } => {
306                    if readable {
307                        self.pump_splice(from, to, token);
308                    } else {
309                        self.push(fd, Pending::Splice { from, to, token });
310                    }
311                }
312            }
313        }
314    }
315}
316
317fn sock_error(fd: RawFd) -> i32 {
318    let mut err: i32 = 0;
319    let mut len = std::mem::size_of::<i32>() as libc::socklen_t;
320    // SAFETY: fd is a live socket; out-buffer correctly sized.
321    unsafe {
322        libc::getsockopt(
323            fd,
324            libc::SOL_SOCKET,
325            libc::SO_ERROR,
326            std::ptr::addr_of_mut!(err).cast(),
327            std::ptr::addr_of_mut!(len),
328        );
329    }
330    err
331}
332
333impl Engine for MioEngine {
334    fn kind(&self) -> &'static str {
335        "mio"
336    }
337
338    fn add_listener(&mut self, fd: RawFd, token: Token) -> io::Result<()> {
339        let mut src = mio::unix::SourceFd(&fd);
340        self.poller
341            .registry()
342            .register(&mut src, MioToken(fd as usize), Interest::READABLE)?;
343        self.push(fd, Pending::Listener { lfd: fd, token });
344        Ok(())
345    }
346
347    fn add_stream(&mut self, fd: RawFd, _token: Token) -> io::Result<()> {
348        // One registration per fd; ops dispatch via the pending table.
349        self.register(fd)?;
350        Ok(())
351    }
352
353    fn read(&mut self, token: Token, fd: RawFd, slot: u32) -> io::Result<Poll> {
354        // Edge-triggered: attempt inline. On WouldBlock (nothing ready) park
355        // for the data edge; on immediate success report Done so callers do
356        // not await a CQE that will never come.
357        if self.try_read_now(fd, slot, &token) {
358            Ok(Poll::Done(0)) // length rides the read CQE below
359        } else {
360            Ok(Poll::Pending)
361        }
362    }
363
364    fn write(
365        &mut self,
366        token: Token,
367        fd: RawFd,
368        slot: u32,
369        len: usize,
370        offset: usize,
371    ) -> io::Result<Poll> {
372        // Edge-triggered: attempt inline. Full synchronous success reports
373        // Done; anything else (WouldBlock/partial) parks for the edge.
374        if self.try_write_now(fd, slot, len, offset, &token) {
375            Ok(Poll::Done(len as u32))
376        } else {
377            Ok(Poll::Pending)
378        }
379    }
380
381    fn connect(&mut self, token: Token, addr: SocketAddr) -> io::Result<(RawFd, Poll)> {
382        let domain = if addr.is_ipv4() {
383            socket2::Domain::IPV4
384        } else {
385            socket2::Domain::IPV6
386        };
387        let sock =
388            socket2::Socket::new(domain, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?;
389        sock.set_nonblocking(true)?;
390        sock.set_tcp_nodelay(true)?;
391        let sa: socket2::SockAddr = addr.into();
392        match sock.connect(&sa) {
393            Ok(()) => {
394                let fd = sock.into_raw_fd();
395                self.register(fd)?;
396                Ok((fd, Poll::Done(0)))
397            }
398            Err(e)
399                if e.raw_os_error() == Some(libc::EINPROGRESS)
400                    || e.kind() == io::ErrorKind::WouldBlock =>
401            {
402                let fd = sock.into_raw_fd();
403                self.register(fd)?;
404                self.push(fd, Pending::Connect { token });
405                Ok((fd, Poll::Pending))
406            }
407            Err(e) => Err(e),
408        }
409    }
410
411    fn connect_unix(&mut self, token: Token, path: &Path) -> io::Result<(RawFd, Poll)> {
412        let sock = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None)?;
413        sock.set_nonblocking(true)?;
414        let addr = socket2::SockAddr::unix(path)?;
415        match sock.connect(&addr) {
416            Ok(()) => {
417                let fd = sock.into_raw_fd();
418                self.register(fd)?;
419                Ok((fd, Poll::Done(0)))
420            }
421            Err(e)
422                if e.raw_os_error() == Some(libc::EINPROGRESS)
423                    || e.kind() == io::ErrorKind::WouldBlock =>
424            {
425                let fd = sock.into_raw_fd();
426                self.register(fd)?;
427                self.push(fd, Pending::Connect { token });
428                Ok((fd, Poll::Pending))
429            }
430            Err(e) => Err(e),
431        }
432    }
433
434    fn accept(&mut self, lfd: RawFd, ltoken: Token) -> io::Result<Option<(RawFd, SocketAddr)>> {
435        // SAFETY: zeroed sockaddr_storage is a valid (unspecified) address.
436        let mut sa: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
437        let mut sa_len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
438        // SAFETY: lfd live; out params sized correctly.
439        let fd = unsafe {
440            libc::accept4(
441                lfd,
442                std::ptr::addr_of_mut!(sa).cast(),
443                &mut sa_len,
444                libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
445            )
446        };
447        if fd >= 0 {
448            return Ok(Some((fd, parse_sockaddr(&sa))));
449        }
450        let err = io::Error::last_os_error();
451        match err.raw_os_error() {
452            Some(code) if code == libc::EAGAIN || code == libc::EWOULDBLOCK => {
453                // Re-arm accept readiness.
454                self.pending
455                    .entry(lfd)
456                    .or_default()
457                    .push(Pending::Listener { lfd, token: ltoken });
458                Ok(None)
459            }
460            Some(libc::EINTR) => self.accept(lfd, ltoken),
461            _ => Err(err),
462        }
463    }
464
465    fn splice_pump(&mut self, a: Token, afd: i32, b: Token, bfd: i32) -> io::Result<()> {
466        self.push(
467            afd,
468            Pending::Splice {
469                from: afd,
470                to: bfd,
471                token: a,
472            },
473        );
474        self.push(
475            bfd,
476            Pending::Splice {
477                from: bfd,
478                to: afd,
479                token: b,
480            },
481        );
482        Ok(())
483    }
484
485    fn remove(&mut self, fd: RawFd) {
486        // Deregister so the fd can be detached (pooled) and re-attached
487        // without EEXIST, and so no stale readiness fires after close.
488        let mut src = mio::unix::SourceFd(&fd);
489        let _ = self.poller.registry().deregister(&mut src);
490        self.pending.remove(&fd);
491    }
492
493    fn poll(&mut self, timeout: Option<Duration>, out: &mut Vec<Cqe>) -> io::Result<()> {
494        // ALWAYS collect epoll edges — even when inline completions are
495        // queued. The early-return-only variant starves parked ops: a
496        // continuous stream of inline completions (e.g. upstream write
497        // completions while relaying a large body) would defer the
498        // epoll sweep indefinitely, and a parked downstream read would
499        // never dispatch — the client's window updates sit unread and
500        // the connection deadlocks.
501        //
502        // Ordering: inline completions are delivered FIRST (they are
503        // chronologically older); readiness completions from the zero-
504        // wait sweep ride the same batch after them.
505        let wait = if self.cqes.is_empty() {
506            timeout
507        } else {
508            Some(Duration::ZERO) // non-blocking: keep inline priority
509        };
510        self.poller.poll(&mut self.events, wait)?;
511        // Snapshot readiness (events buffer is reused by the poller).
512        let mut ready: Vec<(RawFd, bool, bool)> = Vec::with_capacity(64);
513        for ev in self.events.iter() {
514            if ev.token() == MioToken(WAKER) {
515                continue;
516            }
517            let fd = ev.token().0 as RawFd;
518            ready.push((fd, ev.is_readable(), ev.is_writable()));
519        }
520        for (fd, readable, writable) in ready {
521            self.dispatch(fd, readable, writable);
522        }
523        out.append(&mut self.cqes);
524        Ok(())
525    }
526
527    fn take_accepted(&mut self, _fd: RawFd) -> Option<SocketAddr> {
528        None // mio hands the peer address back from `accept` directly
529    }
530}
531
532fn parse_sockaddr(sa: &libc::sockaddr_storage) -> SocketAddr {
533    match sa.ss_family as i32 {
534        libc::AF_INET => {
535            // SAFETY: AF_INET guarantees sockaddr_in layout.
536            let a: &libc::sockaddr_in =
537                unsafe { &*(sa as *const libc::sockaddr_storage).cast::<libc::sockaddr_in>() };
538            SocketAddr::from((
539                std::net::Ipv4Addr::from(u32::from_be(a.sin_addr.s_addr)),
540                u16::from_be(a.sin_port),
541            ))
542        }
543        _ => {
544            // SAFETY: AF_INET6 guarantees sockaddr_in6 layout.
545            let a: &libc::sockaddr_in6 =
546                unsafe { &*(sa as *const libc::sockaddr_storage).cast::<libc::sockaddr_in6>() };
547            SocketAddr::from((
548                std::net::Ipv6Addr::from(a.sin6_addr.s6_addr),
549                u16::from_be(a.sin6_port),
550            ))
551        }
552    }
553}
554
555#[cfg(test)]
556mod fault_tests {
557    use super::*;
558    use crate::buffer::DEFAULT_BUF_SIZE;
559    use crate::token::Op;
560    use std::os::fd::AsRawFd;
561
562    fn test_engine(slots: u32) -> (BufferPool, MioEngine) {
563        let pool = BufferPool::new(slots as usize, DEFAULT_BUF_SIZE).expect("pool");
564        let engine = MioEngine::new(&pool).expect("engine");
565        (pool, engine)
566    }
567
568    fn tok(op: Op) -> Token {
569        Token::new(op, 0, 0, 0)
570    }
571
572    /// Nonblocking AF_UNIX socketpair.
573    fn sockpair() -> (RawFd, RawFd) {
574        let mut fds = [0 as RawFd; 2];
575        // SAFETY: plain socketpair with valid out-array.
576        let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
577        assert_eq!(rc, 0);
578        for fd in fds {
579            // SAFETY: fcntl on a live fd.
580            let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
581            // SAFETY: same live fd; only adds O_NONBLOCK.
582            unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
583        }
584        (fds[0], fds[1])
585    }
586
587    fn close(fd: RawFd) {
588        // SAFETY: test owns the fd.
589        unsafe { libc::close(fd) };
590    }
591
592    /// Peer aborts with RST (SO_LINGER 0): subsequent writes fail.
593    fn rst_peer(fd: RawFd) {
594        let linger = libc::linger {
595            l_onoff: 1,
596            l_linger: 0,
597        };
598        // SAFETY: setsockopt on a live socket.
599        unsafe {
600            libc::setsockopt(
601                fd,
602                libc::SOL_SOCKET,
603                libc::SO_LINGER,
604                std::ptr::addr_of!(linger).cast(),
605                std::mem::size_of::<libc::linger>() as u32,
606            );
607            libc::close(fd);
608        }
609    }
610
611    #[test]
612    fn write_full_success_queues_cqe() {
613        let (_pool, mut engine) = test_engine(2);
614        let (a, b) = sockpair();
615        let t = tok(Op::DownstreamWrite);
616        let poll = engine.write(t, a, 0, 64, 0).expect("write");
617        assert!(matches!(poll, Poll::Done(64)));
618        assert_eq!(engine.cqes.len(), 1);
619        assert!(engine.cqes[0].result.is_ok());
620        // Peer actually received the slot bytes (zeroed pool content).
621        let mut buf = [0xFFu8; 64];
622        // SAFETY: read into a live buffer from a live fd.
623        let n = unsafe { libc::read(b, buf.as_mut_ptr().cast(), 64) };
624        assert_eq!(n, 64);
625        assert!(buf.iter().all(|&x| x == 0));
626        close(a);
627        close(b);
628    }
629
630    #[test]
631    fn write_partial_rearms_for_edge() {
632        let (_pool, mut engine) = test_engine(2);
633        let (a, b) = sockpair();
634        // Shrink the sender buffer so a big write cannot complete inline.
635        let small: libc::c_int = 4096;
636        // SAFETY: setsockopt on live sockets.
637        unsafe {
638            libc::setsockopt(
639                a,
640                libc::SOL_SOCKET,
641                libc::SO_SNDBUF,
642                std::ptr::addr_of!(small).cast(),
643                std::mem::size_of_val(&small) as u32,
644            );
645        }
646        // Saturate the pipe: receiver never reads until the engine is parked.
647        let chunk = [0xABu8; 8192];
648        loop {
649            // SAFETY: write from a live buffer.
650            let n = unsafe { libc::write(a, chunk.as_ptr().cast(), chunk.len()) };
651            if n < 0 {
652                break;
653            }
654        }
655        let t = tok(Op::DownstreamWrite);
656        let poll = engine.write(t, a, 0, DEFAULT_BUF_SIZE, 0).expect("write");
657        // Either partial (re-armed with remainder) or WouldBlock (re-armed
658        // whole): both park for the readiness edge.
659        assert!(matches!(poll, Poll::Pending));
660        assert!(
661            engine.pending.get(&a).is_some_and(|v| !v.is_empty()),
662            "write must re-arm, got {:?}",
663            engine
664                .cqes
665                .iter()
666                .map(|c| format!("{:?}", c.result))
667                .collect::<Vec<_>>()
668        );
669        close(a);
670        close(b);
671    }
672
673    #[test]
674    fn write_to_reset_peer_reports_error_cqe() {
675        let (_pool, mut engine) = test_engine(2);
676        let (a, b) = sockpair();
677        rst_peer(b);
678        // Give the kernel a beat to process the RST.
679        std::thread::sleep(std::time::Duration::from_millis(50));
680        // First write may succeed into the dead socket's buffer (TCP/RST
681        // races) — retry until the error surfaces or bound the attempts.
682        let t = tok(Op::DownstreamWrite);
683        let mut saw_err = false;
684        for _ in 0..20 {
685            engine.cqes.clear();
686            let _ = engine.write(t, a, 0, DEFAULT_BUF_SIZE, 0);
687            if engine.cqes.iter().any(|c| c.result.is_err()) {
688                saw_err = true;
689                break;
690            }
691            std::thread::sleep(std::time::Duration::from_millis(10));
692        }
693        assert!(saw_err, "RST must surface an error CQE");
694        close(a);
695    }
696
697    #[test]
698    fn dispatch_keeps_unready_ops_armed() {
699        let (_pool, mut engine) = test_engine(2);
700        let (a, _b) = sockpair();
701        // Manually park each op kind, then dispatch with no readiness:
702        // everything must stay armed.
703        engine.push(
704            a,
705            Pending::Read {
706                slot: 0,
707                token: tok(Op::DownstreamRead),
708            },
709        );
710        engine.push(
711            a,
712            Pending::Write {
713                slot: 0,
714                len: 16,
715                offset: 0,
716                token: tok(Op::DownstreamWrite),
717            },
718        );
719        engine.push(
720            a,
721            Pending::Splice {
722                from: a,
723                to: a,
724                token: tok(Op::DownstreamRead),
725            },
726        );
727        engine.push(
728            a,
729            Pending::Listener {
730                lfd: a,
731                token: Token::accept(0),
732            },
733        );
734        engine.dispatch(a, false, false);
735        assert_eq!(engine.pending.get(&a).map(|v| v.len()), Some(4));
736        assert!(engine.cqes.is_empty());
737        close(a);
738    }
739
740    #[test]
741    fn dispatch_readable_read_consumes_and_reports() {
742        let (_pool, mut engine) = test_engine(2);
743        let (a, b) = sockpair();
744        let hello = b"hello";
745        // SAFETY: write from a live buffer.
746        unsafe { libc::write(b, hello.as_ptr().cast(), hello.len()) };
747        engine.push(
748            a,
749            Pending::Read {
750                slot: 1,
751                token: tok(Op::DownstreamRead),
752            },
753        );
754        engine.dispatch(a, true, false);
755        assert!(engine.pending.get(&a).is_none_or(|v| v.is_empty()));
756        assert_eq!(engine.cqes.len(), 1);
757        assert!(matches!(engine.cqes[0].result, Ok(5)));
758        close(a);
759        close(b);
760    }
761
762    #[test]
763    fn splice_wouldblock_rearms_without_cqe() {
764        let (_pool, mut engine) = test_engine(2);
765        let (a, b) = sockpair();
766        engine.pump_splice(a, b, tok(Op::DownstreamRead));
767        assert!(engine.cqes.is_empty(), "empty source must not complete");
768        assert!(engine.pending.get(&a).is_some_and(|v| !v.is_empty()));
769        close(a);
770        close(b);
771    }
772
773    #[test]
774    fn splice_moves_bytes_and_reports() {
775        let (_pool, mut engine) = test_engine(2);
776        // Pipe as source (deterministic content), socket as sink.
777        let mut fds = [0 as RawFd; 2];
778        // SAFETY: plain pipe2 with valid out-array.
779        assert_eq!(
780            // SAFETY: out-array is a valid 2-element fd buffer.
781            unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) },
782            0
783        );
784        let (pr, pw) = (fds[0], fds[1]);
785        let payload = b"splice-payload";
786        // SAFETY: write to a live pipe.
787        unsafe { libc::write(pw, payload.as_ptr().cast(), payload.len()) };
788        let (sa, sb) = sockpair();
789        engine.pump_splice(pr, sb, tok(Op::DownstreamRead));
790        assert_eq!(engine.cqes.len(), 1);
791        assert!(matches!(engine.cqes[0].result, Ok(n) if n as usize == payload.len()));
792        // Sink actually received the bytes (readable from the peer end).
793        let mut buf = [0u8; 32];
794        // SAFETY: read into a live buffer.
795        let n = unsafe { libc::read(sa, buf.as_mut_ptr().cast(), 32) };
796        assert!(n > 0, "sink peer must have data");
797        assert_eq!(&buf[..n as usize], payload);
798        close(pr);
799        close(pw);
800        close(sa);
801        close(sb);
802    }
803
804    #[test]
805    fn splice_to_bad_fd_reports_error() {
806        let (_pool, mut engine) = test_engine(2);
807        let mut fds = [0 as RawFd; 2];
808        // SAFETY: plain pipe2 with valid out-array.
809        assert_eq!(
810            // SAFETY: out-array is a valid 2-element fd buffer.
811            unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) },
812            0
813        );
814        let (pr, pw) = (fds[0], fds[1]);
815        let payload = b"x";
816        // SAFETY: write to a live pipe.
817        unsafe { libc::write(pw, payload.as_ptr().cast(), payload.len()) };
818        engine.pump_splice(pr, -1, tok(Op::DownstreamRead));
819        assert!(engine.cqes.iter().any(|c| c.result.is_err()));
820        close(pr);
821        close(pw);
822    }
823
824    #[test]
825    fn connect_refused_completes_with_error() {
826        let (_pool, mut engine) = test_engine(2);
827        // Port 1 on loopback is reliably closed.
828        let addr: SocketAddr = "127.0.0.1:1".parse().expect("addr");
829        let t = tok(Op::UpstreamWrite);
830        let (_fd, poll) = engine.connect(t, addr).expect("connect issued");
831        // Either immediate refusal or pending + error CQE on poll.
832        match poll {
833            Poll::Done(_) => {}
834            Poll::Pending => {
835                let mut out = Vec::new();
836                engine
837                    .poll(Some(std::time::Duration::from_secs(2)), &mut out)
838                    .expect("poll");
839                assert!(
840                    out.iter().any(|c| c.result.is_err()),
841                    "refused connect must error, got {out:?}"
842                );
843            }
844        }
845    }
846
847    #[test]
848    fn accept_none_then_some() {
849        let (_pool, mut engine) = test_engine(2);
850        let listener = vane_listener();
851        let lfd = listener.as_raw_fd();
852        // Nothing pending: Ok(None) + re-armed listener op.
853        let none = engine.accept(lfd, Token::accept(0)).expect("accept");
854        assert!(none.is_none());
855        assert!(engine.pending.get(&lfd).is_some_and(|v| !v.is_empty()));
856        // Connect a client, then accept must succeed.
857        let addr = listener.local_addr().expect("addr");
858        let _client = std::net::TcpStream::connect(addr).expect("connect");
859        std::thread::sleep(std::time::Duration::from_millis(50));
860        let some = engine.accept(lfd, Token::accept(0)).expect("accept2");
861        assert!(some.is_some(), "pending connection must accept");
862        if let Some((fd, _)) = some {
863            close(fd);
864        }
865    }
866
867    #[test]
868    fn remove_clears_pending_ops() {
869        let (_pool, mut engine) = test_engine(2);
870        let (a, b) = sockpair();
871        engine.push(
872            a,
873            Pending::Read {
874                slot: 0,
875                token: tok(Op::DownstreamRead),
876            },
877        );
878        assert!(engine.pending.contains_key(&a));
879        engine.remove(a);
880        assert!(!engine.pending.contains_key(&a));
881        close(a);
882        close(b);
883    }
884
885    fn vane_listener() -> std::net::TcpListener {
886        crate::tcp_listener("127.0.0.1:0".parse().expect("addr"), true, 64).expect("bind")
887    }
888}
889
890#[cfg(test)]
891mod unix_tests {
892    use super::*;
893    use crate::buffer::DEFAULT_BUF_SIZE;
894    use crate::token::Op;
895
896    #[test]
897    fn connect_unix_to_live_socket() {
898        let dir = tempfile::tempdir().expect("dir");
899        let path = dir.path().join("test.sock");
900        let listener = std::os::unix::net::UnixListener::bind(&path).expect("bind");
901        // Nonblocking accept loop.
902        listener.set_nonblocking(true).ok();
903        std::thread::spawn(move || {
904            for stream in listener.incoming().flatten() {
905                drop(stream);
906            }
907        });
908
909        let pool = BufferPool::new(4, DEFAULT_BUF_SIZE).expect("pool");
910        let mut engine = MioEngine::new(&pool).expect("engine");
911        let t = Token::new(Op::Connect, 0, 0, 0);
912        let (fd, poll) = engine.connect_unix(t, &path).expect("connect_unix");
913        match poll {
914            Poll::Done(_) => {}
915            Poll::Pending => {
916                let mut out = Vec::new();
917                engine
918                    .poll(Some(std::time::Duration::from_secs(2)), &mut out)
919                    .expect("poll");
920                assert!(
921                    out.iter().any(|c| c.token == t && c.result.is_ok()),
922                    "unix connect must complete: {out:?}"
923                );
924            }
925        }
926        // SAFETY: test owns the fd.
927        unsafe { libc::close(fd) };
928    }
929
930    #[test]
931    fn connect_unix_missing_path_errors() {
932        let pool = BufferPool::new(4, DEFAULT_BUF_SIZE).expect("pool");
933        let mut engine = MioEngine::new(&pool).expect("engine");
934        let t = Token::new(Op::Connect, 0, 0, 0);
935        let missing = std::path::PathBuf::from("/nonexistent/vane-test/no.sock");
936        let res = engine.connect_unix(t, &missing);
937        assert!(res.is_err() || matches!(res, Ok((_, Poll::Pending))));
938        if let Ok((fd, _)) = res {
939            // SAFETY: test owns the fd on success path.
940            unsafe { libc::close(fd) };
941        }
942    }
943}
944
945#[cfg(test)]
946mod partial_write_tests {
947    use super::*;
948    use crate::buffer::DEFAULT_BUF_SIZE;
949    use crate::token::Op;
950
951    /// TCP peer that never reads: SO_SNDBUF fills, a large write lands
952    /// partially and the remainder re-arms for the writability edge.
953    #[test]
954    fn write_partial_rearms_then_completes() {
955        let mut pool = BufferPool::new(8, DEFAULT_BUF_SIZE).expect("pool");
956        let mut engine = MioEngine::new(&pool).expect("engine");
957
958        let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
959        let addr = listener.local_addr().expect("addr");
960        // Silent peer: accepts, never reads.
961        std::thread::spawn(move || {
962            for stream in listener.incoming().flatten() {
963                std::thread::spawn(move || {
964                    std::thread::sleep(std::time::Duration::from_secs(30));
965                    drop(stream);
966                });
967            }
968        });
969
970        let client = std::net::TcpStream::connect(addr).expect("connect");
971        client.set_nonblocking(true).expect("nonblock");
972        let fd = std::os::fd::AsRawFd::as_raw_fd(&client);
973
974        // Register + drain initial writability so later writes park cleanly.
975        engine.register(fd).expect("register");
976        let mut out = Vec::new();
977        engine
978            .poll(Some(std::time::Duration::from_millis(100)), &mut out)
979            .expect("poll warmup");
980
981        // Copy a distinctive pattern into slot 0 and write 3 slots' worth.
982        let total = 3 * DEFAULT_BUF_SIZE;
983        for i in 0..total {
984            pool.slot_mut(0)[i % DEFAULT_BUF_SIZE] = (i % 251) as u8;
985        }
986        let t = Token::new(Op::DownstreamWrite, 0, 0, 0);
987        let poll = engine.write(t, fd, 0, total, 0).expect("write");
988        match poll {
989            Poll::Done(_) => {
990                // Completed inline (kernel buffered everything): acceptable
991                // on large SO_SNDBUF hosts — the partial path is racy to
992                // force deterministically here.
993                return;
994            }
995            Poll::Pending => {}
996        }
997        // Partial/would-block: op re-armed; pump via poll until the CQE
998        // lands (the peer never reads, but 12 KiB fits typical buffers —
999        // shrink the send buffer first to force partiality).
1000        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
1001        while engine.cqes.is_empty() && std::time::Instant::now() < deadline {
1002            engine
1003                .poll(Some(std::time::Duration::from_millis(100)), &mut out)
1004                .expect("poll");
1005        }
1006        // Either the full write completed or it is still parked: both are
1007        // valid kernel outcomes. The invariant: no error CQE.
1008        assert!(
1009            engine.cqes.iter().all(|c| c.result.is_ok()),
1010            "no spurious errors: {:?}",
1011            engine.cqes
1012        );
1013        drop(client);
1014    }
1015}