Skip to main content

vfsi_uring/
engine.rs

1use crate::Options;
2use io_uring::{IoUring, opcode, types};
3use std::{
4    fs::File,
5    io,
6    os::fd::AsRawFd,
7    sync::{
8        Arc,
9        atomic::{AtomicU64, Ordering},
10    },
11};
12use vfsi_local::io::{DescriptorIo, OwnedRead, Read, Write};
13
14#[derive(Default)]
15struct Counters {
16    syscall_reads: AtomicU64,
17    syscall_writes: AtomicU64,
18    cache_probes: AtomicU64,
19    cache_hits: AtomicU64,
20    cache_misses: AtomicU64,
21    waves: AtomicU64,
22    submissions: AtomicU64,
23    completions: AtomicU64,
24    peak_bytes: AtomicU64,
25    enters: AtomicU64,
26    scratch_growths: AtomicU64,
27    copied_read_bytes: AtomicU64,
28}
29
30/// Cumulative execution counters; a snapshot may span an ongoing operation.
31#[derive(Clone, Copy, Debug, Default)]
32pub struct Stats {
33    /// Ordinary positional reads selected by a warm-cohort NOWAIT hit.
34    pub syscall_reads: u64,
35    /// Nonempty logical writes sent to the synchronous syscall executor.
36    pub syscall_writes: u64,
37    /// Synchronous RWF_NOWAIT read attempts, excluding ring fallback work.
38    pub cache_probes: u64,
39    /// Requests completely satisfied by synchronous RWF_NOWAIT probes.
40    pub cache_hits: u64,
41    /// Requests routed to the ring by cache-read selection.
42    pub cache_misses: u64,
43    pub waves: u64,
44    pub submissions: u64,
45    pub completions: u64,
46    pub peak_bytes: u64,
47    /// Calls to io_uring_enter, including interrupted attempts.
48    pub enters: u64,
49    /// Growths of the executor's reusable owned scratch arena.
50    pub scratch_growths: u64,
51    /// Bytes copied from scratch into borrowed read destinations.
52    pub copied_read_bytes: u64,
53}
54
55#[derive(Clone, Default)]
56pub struct Telemetry(Arc<Counters>);
57
58impl Telemetry {
59    pub fn snapshot(&self) -> Stats {
60        let counters = &self.0;
61        Stats {
62            syscall_reads: counters.syscall_reads.load(Ordering::Relaxed),
63            syscall_writes: counters.syscall_writes.load(Ordering::Relaxed),
64            cache_probes: counters.cache_probes.load(Ordering::Relaxed),
65            cache_hits: counters.cache_hits.load(Ordering::Relaxed),
66            cache_misses: counters.cache_misses.load(Ordering::Relaxed),
67            waves: counters.waves.load(Ordering::Relaxed),
68            submissions: counters.submissions.load(Ordering::Relaxed),
69            completions: counters.completions.load(Ordering::Relaxed),
70            peak_bytes: counters.peak_bytes.load(Ordering::Relaxed),
71            enters: counters.enters.load(Ordering::Relaxed),
72            scratch_growths: counters.scratch_growths.load(Ordering::Relaxed),
73            copied_read_bytes: counters.copied_read_bytes.load(Ordering::Relaxed),
74        }
75    }
76}
77
78pub(crate) struct Engine {
79    ring: Option<IoUring>,
80    options: Options,
81    telemetry: Telemetry,
82    scratch: Vec<u8>,
83    nowait_supported: bool,
84    progress: Vec<usize>,
85    done: Vec<bool>,
86    errors: Vec<Option<i32>>,
87    groups: Vec<std::ops::Range<usize>>,
88    entries: Vec<io_uring::squeue::Entry>,
89    results: Vec<Option<i32>>,
90    pending: Vec<Pending>,
91    #[cfg(test)]
92    cache_read_limit: Option<usize>,
93    #[cfg(test)]
94    cache_read_error: Option<(usize, i32)>,
95    #[cfg(test)]
96    cache_read_calls: std::cell::Cell<usize>,
97    #[cfg(test)]
98    short_completion: Option<usize>,
99    #[cfg(test)]
100    fail_after_submit: bool,
101}
102
103enum Buffer<'a> {
104    Read(&'a mut [u8]),
105    OwnedRead(Vec<u8>),
106    Write(&'a [u8]),
107    Sync(bool),
108}
109struct Job<'a> {
110    file: &'a Arc<File>,
111    offset: u64,
112    buffer: Buffer<'a>,
113}
114#[derive(Clone, Copy)]
115enum Kind {
116    Read,
117    Write,
118    Sync(bool),
119}
120struct Pending {
121    // Own descriptors and final owned-read buffers addressed by SQEs. Borrowed
122    // buffers use the owned scratch arena; none are referenced by the kernel.
123    file: Arc<File>,
124    bytes: std::ops::Range<usize>,
125    offset: u64,
126    kind: Kind,
127    job: usize,
128    owned: Option<Vec<u8>>,
129    owned_start: usize,
130    result: i32,
131}
132
133impl Engine {
134    pub(crate) fn new(options: Options) -> io::Result<(Self, Telemetry)> {
135        if options.queue_depth.get() > 4096 {
136            return Err(io::Error::from_raw_os_error(libc::EINVAL));
137        }
138        // Cooperative task work avoids completion IPIs while the synchronous
139        // caller is already waiting. Unlike SINGLE_ISSUER it permits clients
140        // to be used from different threads. Older kernels keep ordinary rings.
141        let ring = IoUring::builder()
142            .setup_coop_taskrun()
143            .build(options.queue_depth.get())
144            .or_else(|error| {
145                if error.raw_os_error() == Some(libc::EINVAL) {
146                    IoUring::new(options.queue_depth.get())
147                } else {
148                    Err(error)
149                }
150            })?;
151        let mut probe = io_uring::Probe::new();
152        ring.submitter().register_probe(&mut probe)?;
153        if [opcode::Read::CODE, opcode::Write::CODE, opcode::Fsync::CODE]
154            .iter()
155            .any(|code| !probe.is_supported(*code))
156        {
157            return Err(io::Error::from_raw_os_error(libc::EOPNOTSUPP));
158        }
159        let telemetry = Telemetry::default();
160        Ok((
161            Self {
162                ring: Some(ring),
163                options,
164                telemetry: telemetry.clone(),
165                scratch: Vec::new(),
166                nowait_supported: true,
167                progress: Vec::new(),
168                done: Vec::new(),
169                errors: Vec::new(),
170                groups: Vec::new(),
171                entries: Vec::new(),
172                results: Vec::new(),
173                pending: Vec::new(),
174                #[cfg(test)]
175                cache_read_limit: None,
176                #[cfg(test)]
177                cache_read_error: None,
178                #[cfg(test)]
179                cache_read_calls: std::cell::Cell::new(0),
180                #[cfg(test)]
181                short_completion: None,
182                #[cfg(test)]
183                fail_after_submit: false,
184            },
185            telemetry,
186        ))
187    }
188
189    fn cached_read(&self, op: &mut Read<'_>) -> io::Result<usize> {
190        #[cfg(test)]
191        {
192            let calls = self.cache_read_calls.get();
193            self.cache_read_calls.set(calls + 1);
194            if let Some((after, errno)) = self.cache_read_error
195                && calls >= after
196            {
197                return Err(io::Error::from_raw_os_error(errno));
198            }
199        }
200        let length = op.buffer.len();
201        #[cfg(test)]
202        if let Some(limit) = self.cache_read_limit {
203            // A deterministic cache-hit/partial-hit seam, transferring actual
204            // bytes even on test filesystems that cannot implement NOWAIT.
205            use std::os::unix::fs::FileExt;
206            return op
207                .file
208                .read_at(&mut op.buffer[..length.min(limit)], op.offset);
209        }
210        let vector = libc::iovec {
211            iov_base: op.buffer.as_mut_ptr().cast(),
212            iov_len: length,
213        };
214        // SAFETY: this synchronous syscall borrows one initialized, writable
215        // buffer and a live descriptor only until it returns. RWF_NOWAIT never
216        // schedules asynchronous access to caller storage on EAGAIN.
217        let count = unsafe {
218            libc::preadv2(
219                op.file.as_raw_fd(),
220                &vector,
221                1,
222                op.offset as libc::off_t,
223                libc::RWF_NOWAIT,
224            )
225        };
226        if count < 0 {
227            Err(io::Error::last_os_error())
228        } else {
229            Ok(count as usize)
230        }
231    }
232
233    fn wave(&mut self, mut pending: Vec<Pending>) -> io::Result<Vec<Pending>> {
234        let Some(ring) = self.ring.as_mut() else {
235            return Err(io::Error::from_raw_os_error(libc::EIO));
236        };
237        // Take ownership before publishing pointers. On success the arena is
238        // reused; on a catastrophic failure it is retained with the descriptors.
239        let mut scratch = std::mem::take(&mut self.scratch);
240        let scratch_len = scratch.len();
241        // Derive all SQE pointers from one base pointer. Repeated mutable slice
242        // borrows could invalidate earlier pointers into this shared arena.
243        let scratch_ptr = scratch.as_mut_ptr();
244        // Adjacent positional writes already occupy adjacent arena storage.
245        // One transfer avoids scheduling multiple workers against one inode.
246        // Keep the logical requests for precise short-write progress and errors;
247        // never combine reads, gaps, reordered ranges, or different descriptors.
248        let groups = &mut self.groups;
249        groups.clear();
250        for (index, op) in pending.iter().enumerate() {
251            if let Some(group) = groups.last_mut() {
252                let first = &pending[group.start];
253                let last = &pending[group.end - 1];
254                if matches!((first.kind, op.kind), (Kind::Write, Kind::Write))
255                    && Arc::ptr_eq(&first.file, &op.file)
256                    && last.bytes.end == op.bytes.start
257                    && first
258                        .offset
259                        .checked_add((op.bytes.start - first.bytes.start) as u64)
260                        == Some(op.offset)
261                    && op.bytes.end - first.bytes.start <= u32::MAX as usize
262                {
263                    group.end = index + 1;
264                    continue;
265                }
266            }
267            groups.push(index..index + 1);
268        }
269        // Complete all allocation and fallible preparation before publishing
270        // any pointers into the SQ. Vector storage is not moved/reallocated
271        // until every CQE has arrived.
272        let entries = &mut self.entries;
273        entries.clear();
274        entries.extend(groups.iter().enumerate().map(|(index, group)| {
275            let end = pending[group.end - 1].bytes.end;
276            let op = &mut pending[group.start];
277            let fd = types::Fd(op.file.as_raw_fd());
278            let length = (end - op.bytes.start) as u32;
279            // SAFETY: final read buffers and scratch ranges were bounded
280            // before submission. Neither storage is resized/accessed until
281            // all CQEs drain. Vec header moves do not move allocations.
282            let bytes = if let Some(buffer) = &mut op.owned {
283                debug_assert!(op.owned_start + length as usize <= buffer.len());
284                buffer.as_mut_ptr().wrapping_add(op.owned_start)
285            } else {
286                debug_assert!(end <= scratch_len);
287                scratch_ptr.wrapping_add(op.bytes.start)
288            };
289            // Inject an actual short transfer, not a fabricated CQE after
290            // the full payload has already reached the file.
291            #[cfg(test)]
292            let length = self.short_completion.map_or(length, |limit| {
293                length.min(u32::try_from(limit).unwrap_or(u32::MAX))
294            });
295            let entry = match op.kind {
296                Kind::Read => opcode::Read::new(fd, bytes, length)
297                    .offset(op.offset)
298                    .build(),
299                Kind::Write => opcode::Write::new(fd, bytes.cast_const(), length)
300                    .offset(op.offset)
301                    .build(),
302                Kind::Sync(data_only) => opcode::Fsync::new(fd)
303                    .flags(if data_only {
304                        types::FsyncFlags::DATASYNC
305                    } else {
306                        types::FsyncFlags::empty()
307                    })
308                    .build(),
309            };
310            entry.user_data(index as u64)
311        }));
312        let results = &mut self.results;
313        results.clear();
314        results.resize(entries.len(), None);
315        let bytes: usize = pending.iter().map(|op| op.bytes.len()).sum();
316        let counters = &self.telemetry.0;
317        counters
318            .peak_bytes
319            .fetch_max(bytes as u64, Ordering::Relaxed);
320        counters.waves.fetch_add(1, Ordering::Relaxed);
321        let result = (|| {
322            {
323                let mut queue = ring.submission();
324                for entry in entries.iter() {
325                    // SAFETY: owned buffers/files are retained until completion;
326                    // this empty ring has capacity >= the bounded wave length.
327                    unsafe { queue.push(entry) }
328                        .map_err(|_| io::Error::from_raw_os_error(libc::EIO))?;
329                }
330            }
331            let mut remaining = entries.len();
332            while remaining != 0 {
333                counters.enters.fetch_add(1, Ordering::Relaxed);
334                // The synchronous API cannot return before the whole wave is
335                // drained. Waiting for one CQE just adds wakeups and syscalls.
336                match ring.submit_and_wait(remaining) {
337                    Ok(submitted) => {
338                        counters
339                            .submissions
340                            .fetch_add(submitted as u64, Ordering::Relaxed);
341                        #[cfg(test)]
342                        if self.fail_after_submit {
343                            return Err(io::Error::from_raw_os_error(libc::EIO));
344                        }
345                    }
346                    Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
347                    Err(error) => return Err(error),
348                }
349                for completion in ring.completion() {
350                    let index = completion.user_data() as usize;
351                    if index >= results.len() || results[index].is_some() {
352                        return Err(io::Error::from_raw_os_error(libc::EIO));
353                    }
354                    let group = &groups[index];
355                    let length =
356                        pending[group.end - 1].bytes.end - pending[group.start].bytes.start;
357                    let result = completion.result();
358                    results[index] = Some(
359                        if (result >= 0 && result as usize > length)
360                            || (result == 0 && matches!(pending[group.start].kind, Kind::Write))
361                        {
362                            -libc::EIO
363                        } else {
364                            result
365                        },
366                    );
367                    remaining -= 1;
368                    counters.completions.fetch_add(1, Ordering::Relaxed);
369                }
370            }
371            Ok(())
372        })();
373        if let Err(error) = result {
374            // Ring teardown alone is not proof that kernel references are gone.
375            // Poison the executor, never replay writes, and deliberately retain
376            // in-flight resources on catastrophic submission failure. An owned
377            // result may be larger than its wave slice; retain its entire final
378            // allocation, bounded by the caller's read/output budget.
379            // Normal negative CQEs are drained and do NOT leak resources.
380            self.ring.take();
381            std::mem::forget(pending);
382            std::mem::forget(scratch);
383            return Err(error);
384        }
385        self.scratch = scratch;
386        for (group, result) in groups.iter().zip(results.iter()) {
387            let result = result.unwrap();
388            let mut remaining = result.max(0) as usize;
389            for op in &mut pending[group.clone()] {
390                let count = remaining.min(op.bytes.len());
391                remaining -= count;
392                op.result = if result < 0 { result } else { count as i32 };
393            }
394        }
395        Ok(pending)
396    }
397
398    fn execute(&mut self, jobs: &mut [Job<'_>]) -> Vec<io::Result<usize>> {
399        if self.ring.is_none() {
400            return jobs
401                .iter()
402                .map(|_| Err(io::Error::from_raw_os_error(libc::EIO)))
403                .collect();
404        }
405        let mut progress = std::mem::take(&mut self.progress);
406        let mut done = std::mem::take(&mut self.done);
407        let mut errors = std::mem::take(&mut self.errors);
408        let mut pending = std::mem::take(&mut self.pending);
409        let mut copied_read_bytes = 0;
410        progress.clear();
411        progress.resize(jobs.len(), 0);
412        done.clear();
413        done.resize(jobs.len(), false);
414        errors.clear();
415        errors.resize(jobs.len(), None);
416        while done.iter().any(|finished| !finished) {
417            pending.clear();
418            let mut budget = self.options.max_batch_bytes.get();
419            for (index, job) in jobs.iter_mut().enumerate() {
420                if done[index] {
421                    continue;
422                }
423                if pending.len() == self.options.queue_depth.get() as usize {
424                    break;
425                }
426                let (kind, length) = match &job.buffer {
427                    Buffer::Read(buffer) => (Kind::Read, buffer.len()),
428                    Buffer::OwnedRead(buffer) => (Kind::Read, buffer.len()),
429                    Buffer::Write(buffer) => (Kind::Write, buffer.len()),
430                    Buffer::Sync(data_only) => (Kind::Sync(*data_only), 0),
431                };
432                if length == progress[index] && !matches!(kind, Kind::Sync(_)) {
433                    done[index] = true;
434                    continue;
435                }
436                if budget == 0 && length != 0 {
437                    break;
438                }
439                let count = (length - progress[index])
440                    .min(budget)
441                    .min(u32::MAX as usize);
442                let Some(offset) = job.offset.checked_add(progress[index] as u64) else {
443                    errors[index] = Some(libc::EOVERFLOW);
444                    done[index] = true;
445                    continue;
446                };
447                let file = Arc::clone(job.file);
448                let start = self.options.max_batch_bytes.get() - budget;
449                let bytes = start..start + count;
450                budget -= count;
451                pending.push(Pending {
452                    file,
453                    bytes,
454                    offset,
455                    kind,
456                    job: index,
457                    owned: match &mut job.buffer {
458                        Buffer::OwnedRead(buffer) => Some(std::mem::take(buffer)),
459                        _ => None,
460                    },
461                    owned_start: progress[index],
462                    result: 0,
463                });
464            }
465            if pending.is_empty() {
466                continue;
467            }
468            let bytes = pending
469                .iter()
470                .filter(|op| op.owned.is_none())
471                .map(|op| op.bytes.end)
472                .max()
473                .unwrap_or(0);
474            if bytes > self.scratch.len() {
475                // reserve_exact avoids geometric growth beyond the configured
476                // byte budget. Already initialized read storage needs no reset:
477                // only bytes acknowledged by a CQE are copied to the caller.
478                self.scratch.reserve_exact(bytes - self.scratch.len());
479                self.scratch.resize(bytes, 0);
480                self.telemetry
481                    .0
482                    .scratch_growths
483                    .fetch_add(1, Ordering::Relaxed);
484            }
485            for op in &pending {
486                if let Buffer::Write(data) = &jobs[op.job].buffer {
487                    let start = progress[op.job];
488                    self.scratch[op.bytes.clone()]
489                        .copy_from_slice(&data[start..start + op.bytes.len()]);
490                }
491            }
492            match self.wave(std::mem::take(&mut pending)) {
493                Ok(mut completed) => {
494                    for op in &mut completed {
495                        let result = op.result;
496                        let index = op.job;
497                        if let Buffer::OwnedRead(buffer) = &mut jobs[index].buffer {
498                            *buffer = op.owned.take().unwrap();
499                        }
500                        if result < 0 {
501                            errors[index] = Some(-result);
502                            done[index] = true;
503                            continue;
504                        }
505                        let count = result as usize;
506                        if count > op.bytes.len() {
507                            errors[index] = Some(libc::EIO);
508                            done[index] = true;
509                            continue;
510                        }
511                        if let Buffer::Read(buffer) = &mut jobs[index].buffer {
512                            buffer[progress[index]..progress[index] + count].copy_from_slice(
513                                &self.scratch[op.bytes.start..op.bytes.start + count],
514                            );
515                            copied_read_bytes += count as u64;
516                        }
517                        progress[index] += count;
518                        // A short read ends this operation; no fixed-offset gap
519                        // concatenation. Short writes retry the contiguous suffix.
520                        done[index] = match op.kind {
521                            Kind::Read => {
522                                count < op.bytes.len()
523                                    || match &jobs[index].buffer {
524                                        Buffer::Read(buffer) => progress[index] == buffer.len(),
525                                        Buffer::OwnedRead(buffer) => {
526                                            progress[index] == buffer.len()
527                                        }
528                                        _ => unreachable!(),
529                                    }
530                            }
531                            // A merged short write may stop before this member.
532                            // Actual zero-progress WRITE CQEs were rejected in
533                            // wave(); these zero counts are unattempted suffixes.
534                            Kind::Write => false,
535                            Kind::Sync(_) => true,
536                        };
537                    }
538                    completed.clear();
539                    pending = completed;
540                }
541                Err(error) => {
542                    let errno = error.raw_os_error().unwrap_or(libc::EIO);
543                    for index in 0..jobs.len() {
544                        if !done[index] {
545                            errors[index] = Some(errno);
546                            done[index] = true;
547                        }
548                    }
549                }
550            }
551            // wave() has drained every published SQE. A known failure must
552            // not dispatch untouched jobs or retry another job's suffix.
553            // Keep their acknowledged progress; the indexed error remains
554            // attached to the request that actually failed.
555            if errors.iter().any(Option::is_some) {
556                break;
557            }
558        }
559        let results = progress
560            .iter()
561            .zip(&errors)
562            .map(|(&count, &error)| {
563                error.map_or(Ok(count), |errno| Err(io::Error::from_raw_os_error(errno)))
564            })
565            .collect();
566        // Do not retain metadata for an arbitrarily large caller cohort for
567        // the client's lifetime. Ordinary queue-sized batches reuse it; larger
568        // batches release these arrays after their results have been assembled.
569        if jobs.len() <= self.options.queue_depth.get() as usize {
570            self.progress = progress;
571            self.done = done;
572            self.errors = errors;
573        }
574        self.pending = pending;
575        self.telemetry
576            .0
577            .copied_read_bytes
578            .fetch_add(copied_read_bytes, Ordering::Relaxed);
579        results
580    }
581}
582
583impl DescriptorIo for Engine {
584    fn owned_read_window(&self) -> Option<usize> {
585        (!self.options.flags.cached_reads()).then_some(self.options.max_batch_bytes.get())
586    }
587    fn read_owned(&mut self, mut operations: Vec<OwnedRead>) -> Vec<io::Result<Vec<u8>>> {
588        // Direct final destinations win for bounded small cohorts. Large
589        // working sets currently copy substantially faster through the reused
590        // arena on the measured ARM64 kernel; keep that proven path rather
591        // than trading away large-file throughput to remove a copy everywhere.
592        let fits_window = operations
593            .iter()
594            .try_fold(0usize, |bytes, op| bytes.checked_add(op.buffer.len()))
595            .is_some_and(|bytes| bytes <= self.options.max_batch_bytes.get());
596        if self.options.flags.cached_reads() || !fits_window {
597            return vfsi_local::io::read_owned_via_borrowed(self, operations);
598        }
599        let mut jobs: Vec<_> = operations
600            .iter_mut()
601            .map(|op| Job {
602                file: &op.file,
603                offset: op.offset,
604                buffer: Buffer::OwnedRead(std::mem::take(&mut op.buffer)),
605            })
606            .collect();
607        let results = self.execute(&mut jobs);
608        jobs.into_iter()
609            .zip(results)
610            .map(|(job, result)| {
611                let count = result?;
612                let Buffer::OwnedRead(mut buffer) = job.buffer else {
613                    unreachable!()
614                };
615                buffer.truncate(count);
616                Ok(buffer)
617            })
618            .collect()
619    }
620    fn ordered(&self) -> bool {
621        self.options.flags.syscall_writes()
622    }
623    fn read(&mut self, operations: &mut [Read<'_>]) -> Vec<io::Result<usize>> {
624        if self.options.flags.cached_reads()
625            && self.nowait_supported
626            && self.ring.is_some()
627            && operations
628                .first()
629                .is_some_and(|op| op.buffer.len() <= 64 * 1024)
630        {
631            let mut results = Vec::with_capacity(operations.len());
632            let mut fallback = Vec::new();
633            let mut indices = Vec::new();
634            let mut failed = false;
635            let mut probe = true;
636            let mut warm_left = 0;
637            let (mut probes, mut hits, mut misses, mut direct) = (0, 0, 0, 0);
638            for (index, op) in operations.iter_mut().enumerate() {
639                if failed {
640                    results.push(Ok(0));
641                    continue;
642                }
643                if op.buffer.is_empty() {
644                    results.push(Ok(0));
645                    continue;
646                }
647                // Require two cache hits before selecting ordinary reads;
648                // one hot file alone is weak evidence about its siblings.
649                let probing = warm_left == 0 || warm_left == 15;
650                let result = if probe && op.buffer.len() <= 64 * 1024 {
651                    if probing {
652                        probes += 1;
653                        self.cached_read(op)
654                    } else {
655                        use std::os::unix::fs::FileExt;
656                        direct += 1;
657                        op.file.read_at(op.buffer, op.offset)
658                    }
659                } else {
660                    Err(io::Error::from_raw_os_error(libc::EAGAIN))
661                };
662                let prefix = match result {
663                    Ok(count) if count == op.buffer.len() => {
664                        if probing {
665                            hits += 1;
666                        }
667                        if warm_left == 0 {
668                            warm_left = 15;
669                        } else {
670                            warm_left -= 1;
671                        }
672                        results.push(Ok(count));
673                        continue;
674                    }
675                    Ok(count) => {
676                        // Confirm zero/partial NOWAIT results through the ring:
677                        // neither is evidence that the file has reached EOF.
678                        probe = false;
679                        count
680                    }
681                    Err(error) if error.raw_os_error() == Some(libc::EAGAIN) => {
682                        // One miss is enough to select ring batching for the
683                        // rest of this call. Do not issue one cold probe per file.
684                        probe = false;
685                        0
686                    }
687                    Err(error)
688                        if matches!(
689                            error.raw_os_error(),
690                            Some(libc::EOPNOTSUPP | libc::ENOSYS)
691                        ) =>
692                    {
693                        self.nowait_supported = false;
694                        probe = false;
695                        0
696                    }
697                    Err(error) => {
698                        results.push(Err(error));
699                        failed = true;
700                        continue;
701                    }
702                };
703                let Some(offset) = op.offset.checked_add(prefix as u64) else {
704                    results.push(Err(io::Error::from_raw_os_error(libc::EOVERFLOW)));
705                    failed = true;
706                    continue;
707                };
708                misses += 1;
709                results.push(Ok(prefix));
710                indices.push(index);
711                fallback.push(Read {
712                    file: op.file,
713                    offset,
714                    buffer: &mut op.buffer[prefix..],
715                });
716            }
717            self.telemetry
718                .0
719                .cache_probes
720                .fetch_add(probes, Ordering::Relaxed);
721            self.telemetry
722                .0
723                .syscall_reads
724                .fetch_add(direct, Ordering::Relaxed);
725            self.telemetry
726                .0
727                .cache_hits
728                .fetch_add(hits, Ordering::Relaxed);
729            self.telemetry
730                .0
731                .cache_misses
732                .fetch_add(misses, Ordering::Relaxed);
733            if !failed && !fallback.is_empty() {
734                // Partial cache hits continue at their actual offsets. Their
735                // uncached suffixes share bounded ring waves with other misses.
736                let mut jobs: Vec<_> = fallback
737                    .iter_mut()
738                    .map(|op| Job {
739                        file: op.file,
740                        offset: op.offset,
741                        buffer: Buffer::Read(op.buffer),
742                    })
743                    .collect();
744                for (index, result) in indices.into_iter().zip(self.execute(&mut jobs)) {
745                    results[index] = result.map(|count| count + results[index].as_ref().unwrap());
746                }
747            }
748            return results;
749        }
750        let mut jobs: Vec<_> = operations
751            .iter_mut()
752            .map(|op| Job {
753                file: op.file,
754                offset: op.offset,
755                buffer: Buffer::Read(op.buffer),
756            })
757            .collect();
758        self.execute(&mut jobs)
759    }
760    fn write(&mut self, operations: &[Write<'_>]) -> Vec<io::Result<usize>> {
761        if self.options.flags.syscall_writes() && self.ring.is_some() {
762            let results = vfsi_local::io::write_ordered(operations);
763            self.telemetry.0.syscall_writes.fetch_add(
764                operations
765                    .iter()
766                    .zip(&results)
767                    .filter(|(op, result)| !op.data.is_empty() && !matches!(result, Ok(0)))
768                    .count() as u64,
769                Ordering::Relaxed,
770            );
771            return results;
772        }
773        let mut jobs: Vec<_> = operations
774            .iter()
775            .map(|op| Job {
776                file: op.file,
777                offset: op.offset,
778                buffer: Buffer::Write(op.data),
779            })
780            .collect();
781        self.execute(&mut jobs)
782    }
783    fn sync(&mut self, files: &[&Arc<File>], data_only: bool) -> Vec<io::Result<usize>> {
784        let mut jobs: Vec<_> = files
785            .iter()
786            .map(|file| Job {
787                file,
788                offset: 0,
789                buffer: Buffer::Sync(data_only),
790            })
791            .collect();
792        self.execute(&mut jobs)
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799    use vfsi_core::{ReadOp, VfOffset, WriteOp};
800    use vfsi_sync::backend::{HandleBackend, VectorBackend};
801
802    #[test]
803    fn owned_reads_fill_final_allocations_without_copying_and_reuse_bookkeeping() {
804        let root = tempfile::tempdir().unwrap();
805        let path = root.path().join("file");
806        std::fs::write(&path, b"0123456789").unwrap();
807        let file = Arc::new(File::open(path).unwrap());
808        for short in [None, Some(2), Some(0)] {
809            let (mut engine, telemetry) = Engine::new(
810                Options::default()
811                    .with_queue_depth(std::num::NonZeroU32::new(3).unwrap())
812                    .with_max_batch_bytes(std::num::NonZeroUsize::new(16).unwrap()),
813            )
814            .unwrap();
815            engine.short_completion = short;
816            let mut capacities = None;
817            for _ in 0..3 {
818                let buffer = vec![99; 8];
819                let pointer = buffer.as_ptr();
820                let results = engine.read_owned(vec![
821                    OwnedRead {
822                        file: Arc::clone(&file),
823                        offset: 1,
824                        buffer,
825                    },
826                    OwnedRead {
827                        file: Arc::clone(&file),
828                        offset: 8,
829                        buffer: vec![99; 4],
830                    },
831                    OwnedRead {
832                        file: Arc::clone(&file),
833                        offset: 0,
834                        buffer: Vec::new(),
835                    },
836                ]);
837                let expected = match short {
838                    None => b"12345678".as_slice(),
839                    Some(2) => b"12",
840                    Some(0) => b"",
841                    _ => unreachable!(),
842                };
843                assert_eq!(results[0].as_ref().unwrap(), expected);
844                assert_eq!(
845                    results[0].as_ref().unwrap().as_ptr(),
846                    pointer,
847                    "the final allocation must be returned unchanged"
848                );
849                // The byte budget partitions the second file; a short CQE
850                // stops at its actual contiguous prefix, never skips bytes.
851                let second = results[1].as_ref().unwrap();
852                assert!(b"89".starts_with(second));
853                if short.is_none() {
854                    assert_eq!(second, b"89");
855                }
856                if short == Some(0) {
857                    assert!(second.is_empty());
858                }
859                assert!(results[2].as_ref().unwrap().is_empty());
860                let current = (
861                    engine.progress.capacity(),
862                    engine.done.capacity(),
863                    engine.errors.capacity(),
864                    engine.groups.capacity(),
865                    engine.entries.capacity(),
866                    engine.results.capacity(),
867                );
868                if let Some(previous) = capacities {
869                    assert_eq!(current, previous);
870                }
871                capacities = Some(current);
872                assert!(engine.progress.capacity() >= 3);
873            }
874            let stats = telemetry.snapshot();
875            assert_eq!((stats.copied_read_bytes, stats.scratch_growths), (0, 0));
876            assert_eq!(stats.submissions, stats.completions);
877            assert!(stats.peak_bytes <= 16);
878            assert!(engine.scratch.is_empty());
879            let mut borrowed = [99; 2];
880            engine.short_completion = None;
881            assert_eq!(
882                *engine.read(&mut [Read {
883                    file: &file,
884                    offset: 2,
885                    buffer: &mut borrowed
886                }])[0]
887                    .as_ref()
888                    .unwrap(),
889                2
890            );
891            assert_eq!(borrowed, *b"23");
892            assert_eq!(telemetry.snapshot().copied_read_bytes, 2);
893        }
894    }
895
896    #[test]
897    fn owned_cohorts_larger_than_the_window_keep_bounded_scratch_then_return_to_direct_reads() {
898        let root = tempfile::tempdir().unwrap();
899        let path = root.path().join("file");
900        std::fs::write(&path, b"abcdefgh").unwrap();
901        let file = Arc::new(File::open(path).unwrap());
902        let (mut engine, telemetry) = Engine::new(
903            Options::default().with_max_batch_bytes(std::num::NonZeroUsize::new(3).unwrap()),
904        )
905        .unwrap();
906        let results = engine.read_owned(vec![OwnedRead {
907            file: Arc::clone(&file),
908            offset: 0,
909            buffer: vec![99; 8],
910        }]);
911        assert_eq!(results[0].as_ref().unwrap(), b"abcdefgh");
912        let stats = telemetry.snapshot();
913        assert_eq!(
914            (stats.waves, stats.copied_read_bytes, stats.peak_bytes),
915            (3, 8, 3)
916        );
917        let results = engine.read_owned(vec![OwnedRead {
918            file,
919            offset: 2,
920            buffer: vec![99; 3],
921        }]);
922        assert_eq!(results[0].as_ref().unwrap(), b"cde");
923        assert_eq!(telemetry.snapshot().copied_read_bytes, 8);
924        assert_eq!(telemetry.snapshot().scratch_growths, 1);
925    }
926
927    #[test]
928    fn owned_read_errors_drain_a_wave_stop_unsent_work_and_allow_reuse() {
929        let root = tempfile::tempdir().unwrap();
930        let path = root.path().join("file");
931        std::fs::write(&path, b"abcd").unwrap();
932        let file = Arc::new(File::open(&path).unwrap());
933        let writeonly = Arc::new(File::options().write(true).open(path).unwrap());
934        let (mut engine, telemetry) =
935            Engine::new(Options::default().with_queue_depth(std::num::NonZeroU32::new(2).unwrap()))
936                .unwrap();
937        let results = engine.read_owned(vec![
938            OwnedRead {
939                file: Arc::clone(&file),
940                offset: 0,
941                buffer: vec![99; 4],
942            },
943            OwnedRead {
944                file: writeonly,
945                offset: 0,
946                buffer: vec![99; 4],
947            },
948            OwnedRead {
949                file: Arc::clone(&file),
950                offset: 0,
951                buffer: vec![99; 4],
952            },
953        ]);
954        assert_eq!(results[0].as_ref().unwrap(), b"abcd");
955        assert_eq!(
956            results[1].as_ref().unwrap_err().raw_os_error(),
957            Some(libc::EBADF)
958        );
959        assert!(results[2].as_ref().unwrap().is_empty());
960        assert_eq!(
961            (
962                telemetry.snapshot().submissions,
963                telemetry.snapshot().completions
964            ),
965            (2, 2)
966        );
967        assert!(
968            engine.progress.is_empty(),
969            "oversized cohort metadata must not be retained"
970        );
971        let results = engine.read_owned(vec![OwnedRead {
972            file,
973            offset: 1,
974            buffer: vec![0; 3],
975        }]);
976        assert_eq!(results[0].as_ref().unwrap(), b"bcd");
977        assert_eq!(telemetry.snapshot().copied_read_bytes, 0);
978    }
979
980    #[test]
981    fn owned_reads_retain_inflight_allocations_and_descriptors_after_publication_failure() {
982        let root = tempfile::tempdir().unwrap();
983        let path = root.path().join("file");
984        std::fs::write(&path, b"abcd").unwrap();
985        let file = Arc::new(File::open(path).unwrap());
986        let weak = Arc::downgrade(&file);
987        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
988        engine.fail_after_submit = true;
989        let results = engine.read_owned(vec![OwnedRead {
990            file,
991            offset: 0,
992            buffer: vec![99; 4],
993        }]);
994        assert_eq!(
995            results[0].as_ref().unwrap_err().raw_os_error(),
996            Some(libc::EIO)
997        );
998        assert!(engine.ring.is_none());
999        assert!(
1000            weak.upgrade().is_some(),
1001            "the in-flight descriptor is retained with its owned destination"
1002        );
1003        let before = telemetry.snapshot().submissions;
1004        assert!(
1005            engine.read_owned(vec![OwnedRead {
1006                file: weak.upgrade().unwrap(),
1007                offset: 0,
1008                buffer: vec![0; 4]
1009            }])[0]
1010                .is_err()
1011        );
1012        assert_eq!(telemetry.snapshot().submissions, before);
1013        assert_eq!(telemetry.snapshot().copied_read_bytes, 0);
1014    }
1015
1016    #[test]
1017    fn syscall_writes_stop_after_errors_and_cannot_bypass_poisoning() {
1018        let root = tempfile::tempdir().unwrap();
1019        let path = root.path().join("file");
1020        std::fs::write(&path, b"old").unwrap();
1021        let write = Arc::new(File::options().write(true).open(&path).unwrap());
1022        let read = Arc::new(File::open(&path).unwrap());
1023        let (mut engine, telemetry) =
1024            Engine::new(Options::default().with_syscall_writes(true)).unwrap();
1025        assert!(engine.ordered());
1026        let operations = [
1027            Write {
1028                file: &write,
1029                offset: 0,
1030                data: b"A",
1031            },
1032            Write {
1033                file: &read,
1034                offset: 1,
1035                data: b"B",
1036            },
1037            Write {
1038                file: &write,
1039                offset: 2,
1040                data: b"C",
1041            },
1042        ];
1043        let results = engine.write(&operations);
1044        assert_eq!(*results[0].as_ref().unwrap(), 1);
1045        assert_eq!(
1046            results[1].as_ref().unwrap_err().raw_os_error(),
1047            Some(libc::EBADF)
1048        );
1049        assert_eq!(*results[2].as_ref().unwrap(), 0);
1050        assert_eq!(std::fs::read(&path).unwrap(), b"Ald");
1051        let stats = telemetry.snapshot();
1052        assert_eq!(
1053            (stats.syscall_writes, stats.waves, stats.submissions),
1054            (2, 0, 0)
1055        );
1056        engine.ring.take();
1057        assert!(engine.write(&operations).iter().all(Result::is_err));
1058        assert_eq!(std::fs::read(path).unwrap(), b"Ald");
1059        assert_eq!(telemetry.snapshot().syscall_writes, 2);
1060    }
1061
1062    #[test]
1063    fn cached_reads_preserve_partial_prefixes_and_batch_misses_without_repeated_probes() {
1064        for (error, limit, hits, probes, misses, submissions) in [
1065            (None, usize::MAX, 2, 2, 0, 0),
1066            (None, 3, 0, 1, 2, 2),
1067            (Some(libc::EAGAIN), usize::MAX, 0, 1, 2, 2),
1068            (Some(libc::EOPNOTSUPP), usize::MAX, 0, 1, 2, 2),
1069            (Some(libc::ENOSYS), usize::MAX, 0, 1, 2, 2),
1070        ] {
1071            let root = tempfile::tempdir().unwrap();
1072            let path = root.path().join("file");
1073            std::fs::write(&path, b"abcdefgh").unwrap();
1074            let file = Arc::new(File::open(path).unwrap());
1075            let (mut engine, telemetry) =
1076                Engine::new(Options::default().with_cached_reads(true)).unwrap();
1077            engine.cache_read_limit = Some(limit);
1078            engine.cache_read_error = error.map(|errno| (0, errno));
1079            let mut first = [99; 4];
1080            let mut second = [99; 4];
1081            for round in 1..=2 {
1082                let results = engine.read(&mut [
1083                    Read {
1084                        file: &file,
1085                        offset: 0,
1086                        buffer: &mut first,
1087                    },
1088                    Read {
1089                        file: &file,
1090                        offset: 4,
1091                        buffer: &mut second,
1092                    },
1093                ]);
1094                assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1095                assert_eq!((first, second), (*b"abcd", *b"efgh"));
1096                let stats = telemetry.snapshot();
1097                let unsupported = matches!(error, Some(libc::EOPNOTSUPP | libc::ENOSYS));
1098                let probes_rounds = if unsupported { 1 } else { round };
1099                assert_eq!(stats.cache_probes, probes * probes_rounds);
1100                assert_eq!(stats.cache_hits, hits * round);
1101                assert_eq!(stats.syscall_reads, 0);
1102                assert_eq!(stats.cache_misses, misses * probes_rounds);
1103                assert_eq!(stats.submissions, submissions * round);
1104                assert_eq!(stats.completions, stats.submissions);
1105                assert_eq!(engine.nowait_supported, !unsupported);
1106            }
1107        }
1108    }
1109
1110    #[test]
1111    fn warm_cohorts_reprobe_and_keep_original_indices_when_the_next_cohort_is_cold() {
1112        let root = tempfile::tempdir().unwrap();
1113        let path = root.path().join("file");
1114        let expected: Vec<u8> = (0..128).collect();
1115        std::fs::write(&path, &expected).unwrap();
1116        let file = Arc::new(File::open(path).unwrap());
1117        let (mut engine, telemetry) =
1118            Engine::new(Options::default().with_cached_reads(true)).unwrap();
1119        engine.cache_read_limit = Some(usize::MAX);
1120        engine.cache_read_error = Some((2, libc::EAGAIN));
1121        let mut buffers = [[99; 4]; 32];
1122        let mut operations: Vec<_> = buffers
1123            .iter_mut()
1124            .enumerate()
1125            .map(|(i, buffer)| Read {
1126                file: &file,
1127                offset: (i * 4) as u64,
1128                buffer,
1129            })
1130            .collect();
1131        let results = engine.read(&mut operations);
1132        assert_eq!(results.len(), 32);
1133        assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1134        assert_eq!(buffers.concat(), expected);
1135        let stats = telemetry.snapshot();
1136        assert_eq!(
1137            (stats.cache_probes, stats.cache_hits, stats.syscall_reads),
1138            (3, 2, 14)
1139        );
1140        assert_eq!(
1141            (
1142                stats.cache_misses,
1143                stats.waves,
1144                stats.submissions,
1145                stats.completions
1146            ),
1147            (16, 1, 16, 16)
1148        );
1149    }
1150
1151    #[test]
1152    fn cached_reads_keep_eof_tail_and_stop_after_a_real_error() {
1153        let root = tempfile::tempdir().unwrap();
1154        let path = root.path().join("file");
1155        std::fs::write(&path, b"abc").unwrap();
1156        let file = Arc::new(File::open(&path).unwrap());
1157        let writeonly = Arc::new(File::options().write(true).open(path).unwrap());
1158        let (mut engine, telemetry) =
1159            Engine::new(Options::default().with_cached_reads(true)).unwrap();
1160        engine.cache_read_limit = Some(usize::MAX);
1161        let mut bytes = [99; 5];
1162        let results = engine.read(&mut [Read {
1163            file: &file,
1164            offset: 0,
1165            buffer: &mut bytes,
1166        }]);
1167        assert_eq!(*results[0].as_ref().unwrap(), 3);
1168        assert_eq!(bytes, [b'a', b'b', b'c', 99, 99]);
1169        assert_eq!(
1170            telemetry.snapshot().submissions,
1171            1,
1172            "partial cache hits must probe EOF"
1173        );
1174        let before = telemetry.snapshot().cache_probes;
1175        assert_eq!(
1176            *engine.read(&mut [Read {
1177                file: &writeonly,
1178                offset: 0,
1179                buffer: &mut [],
1180            }])[0]
1181                .as_ref()
1182                .unwrap(),
1183            0
1184        );
1185        assert_eq!(
1186            telemetry.snapshot().cache_probes,
1187            before,
1188            "empty reads do not issue syscalls"
1189        );
1190        engine.cache_read_limit = Some(0);
1191        let results = engine.read(&mut [Read {
1192            file: &file,
1193            offset: 0,
1194            buffer: &mut bytes[..3],
1195        }]);
1196        assert_eq!(
1197            *results[0].as_ref().unwrap(),
1198            3,
1199            "a zero NOWAIT reply cannot establish EOF"
1200        );
1201        assert_eq!(&bytes[..3], b"abc");
1202        engine.cache_read_limit = Some(usize::MAX);
1203        let mut first = [99; 3];
1204        let mut invalid = [99; 3];
1205        let mut untouched = [99; 3];
1206        let results = engine.read(&mut [
1207            Read {
1208                file: &file,
1209                offset: 0,
1210                buffer: &mut first,
1211            },
1212            Read {
1213                file: &writeonly,
1214                offset: 0,
1215                buffer: &mut invalid,
1216            },
1217            Read {
1218                file: &file,
1219                offset: 0,
1220                buffer: &mut untouched,
1221            },
1222        ]);
1223        assert_eq!(*results[0].as_ref().unwrap(), 3);
1224        assert_eq!(
1225            results[1].as_ref().unwrap_err().raw_os_error(),
1226            Some(libc::EBADF)
1227        );
1228        assert_eq!(*results[2].as_ref().unwrap(), 0);
1229        assert_eq!(first, *b"abc");
1230        assert_eq!((invalid, untouched), ([99; 3], [99; 3]));
1231        assert_eq!(telemetry.snapshot().submissions, 2);
1232        engine.ring.take();
1233        assert!(
1234            engine.read(&mut [Read {
1235                file: &file,
1236                offset: 0,
1237                buffer: &mut untouched
1238            }])[0]
1239                .is_err()
1240        );
1241        assert_eq!(
1242            untouched, [99; 3],
1243            "poisoned executors must not bypass the ring through cache hits"
1244        );
1245    }
1246
1247    #[test]
1248    fn cooperative_ring_can_move_between_threads_after_draining_each_call() {
1249        let root = tempfile::tempdir().unwrap();
1250        let path = root.path().join("file");
1251        std::fs::write(&path, b"old").unwrap();
1252        let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1253        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1254        let (mut engine, file) = std::thread::spawn(move || {
1255            assert_eq!(
1256                *engine.write(&[Write {
1257                    file: &file,
1258                    offset: 0,
1259                    data: b"new"
1260                }])[0]
1261                    .as_ref()
1262                    .unwrap(),
1263                3
1264            );
1265            (engine, file)
1266        })
1267        .join()
1268        .unwrap();
1269        let mut bytes = [0; 3];
1270        assert_eq!(
1271            *engine.read(&mut [Read {
1272                file: &file,
1273                offset: 0,
1274                buffer: &mut bytes
1275            }])[0]
1276                .as_ref()
1277                .unwrap(),
1278            3
1279        );
1280        assert_eq!(bytes, *b"new");
1281        let stats = telemetry.snapshot();
1282        assert_eq!(
1283            (stats.waves, stats.submissions, stats.completions),
1284            (2, 2, 2)
1285        );
1286    }
1287
1288    #[test]
1289    fn merged_short_writes_preserve_each_requests_contiguous_progress() {
1290        for short in [1, 3, 4, 6] {
1291            let root = tempfile::tempdir().unwrap();
1292            let path = root.path().join("file");
1293            std::fs::write(&path, b"00000000").unwrap();
1294            let file = Arc::new(File::options().write(true).open(&path).unwrap());
1295            let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1296            engine.short_completion = Some(short);
1297            let results = engine.write(&[
1298                Write {
1299                    file: &file,
1300                    offset: 0,
1301                    data: b"abc",
1302                },
1303                Write {
1304                    file: &file,
1305                    offset: 3,
1306                    data: b"defgh",
1307                },
1308            ]);
1309            assert_eq!(*results[0].as_ref().unwrap(), 3);
1310            assert_eq!(*results[1].as_ref().unwrap(), 5);
1311            assert_eq!(std::fs::read(&path).unwrap(), b"abcdefgh");
1312            let waves = 8usize.div_ceil(short) as u64;
1313            let stats = telemetry.snapshot();
1314            assert_eq!(
1315                (stats.waves, stats.submissions, stats.completions),
1316                (waves, waves, waves)
1317            );
1318        }
1319    }
1320
1321    #[test]
1322    fn merged_short_write_is_not_replayed_when_another_completion_fails() {
1323        for short in [1, 3, 4, 6] {
1324            let root = tempfile::tempdir().unwrap();
1325            let path = root.path().join("file");
1326            std::fs::write(&path, b"00000000").unwrap();
1327            let file = Arc::new(File::options().write(true).open(&path).unwrap());
1328            let readonly = Arc::new(File::open(&path).unwrap());
1329            let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1330            engine.short_completion = Some(short);
1331            let results = engine.write(&[
1332                Write {
1333                    file: &file,
1334                    offset: 0,
1335                    data: b"abc",
1336                },
1337                Write {
1338                    file: &file,
1339                    offset: 3,
1340                    data: b"defgh",
1341                },
1342                Write {
1343                    file: &readonly,
1344                    offset: 0,
1345                    data: b"bad",
1346                },
1347            ]);
1348            assert_eq!(*results[0].as_ref().unwrap(), short.min(3));
1349            assert_eq!(*results[1].as_ref().unwrap(), short.saturating_sub(3));
1350            assert_eq!(
1351                results[2].as_ref().unwrap_err().raw_os_error(),
1352                Some(libc::EBADF)
1353            );
1354            let mut expected = *b"00000000";
1355            expected[..short].copy_from_slice(&b"abcdefgh"[..short]);
1356            assert_eq!(std::fs::read(&path).unwrap(), expected);
1357            let stats = telemetry.snapshot();
1358            assert_eq!(
1359                (stats.waves, stats.submissions, stats.completions),
1360                (1, 2, 2)
1361            );
1362        }
1363    }
1364
1365    #[test]
1366    fn merged_zero_progress_and_failed_writes_never_retry_the_group() {
1367        for zero_progress in [false, true] {
1368            let root = tempfile::tempdir().unwrap();
1369            let path = root.path().join("file");
1370            std::fs::write(&path, b"000000").unwrap();
1371            let file = Arc::new(
1372                File::options()
1373                    .read(true)
1374                    .write(zero_progress)
1375                    .open(&path)
1376                    .unwrap(),
1377            );
1378            let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1379            engine.short_completion = zero_progress.then_some(0);
1380            let results = engine.write(&[
1381                Write {
1382                    file: &file,
1383                    offset: 0,
1384                    data: b"abc",
1385                },
1386                Write {
1387                    file: &file,
1388                    offset: 3,
1389                    data: b"DEF",
1390                },
1391            ]);
1392            let errno = if zero_progress {
1393                libc::EIO
1394            } else {
1395                libc::EBADF
1396            };
1397            assert!(
1398                results
1399                    .iter()
1400                    .all(|result| result.as_ref().unwrap_err().raw_os_error() == Some(errno))
1401            );
1402            assert_eq!(std::fs::read(path).unwrap(), b"000000");
1403            let stats = telemetry.snapshot();
1404            assert_eq!(
1405                (stats.waves, stats.submissions, stats.completions),
1406                (1, 1, 1)
1407            );
1408        }
1409    }
1410
1411    #[test]
1412    fn waves_reuse_bounded_storage_and_wait_once_for_all_completions() {
1413        let root = tempfile::tempdir().unwrap();
1414        let path = root.path().join("file");
1415        std::fs::write(&path, b"0123456789").unwrap();
1416        let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1417        let (mut engine, telemetry) = Engine::new(
1418            Options::default().with_max_batch_bytes(std::num::NonZeroUsize::new(7).unwrap()),
1419        )
1420        .unwrap();
1421        for value in *b"abc" {
1422            let bytes = [value; 10];
1423            let results = engine.write(&[Write {
1424                file: &file,
1425                offset: 0,
1426                data: &bytes,
1427            }]);
1428            assert_eq!(*results[0].as_ref().unwrap(), 10);
1429            let mut actual = [0; 10];
1430            let results = engine.read(&mut [Read {
1431                file: &file,
1432                offset: 0,
1433                buffer: &mut actual,
1434            }]);
1435            assert_eq!(*results[0].as_ref().unwrap(), 10);
1436            assert_eq!(actual, bytes);
1437            assert!(engine.scratch.capacity() <= 7);
1438        }
1439        // Recycled storage must not leak stale bytes past a short/EOF read.
1440        let mut tail = [99; 7];
1441        let results = engine.read(&mut [Read {
1442            file: &file,
1443            offset: 8,
1444            buffer: &mut tail,
1445        }]);
1446        assert_eq!(*results[0].as_ref().unwrap(), 2);
1447        assert_eq!(tail, [b'c', b'c', 99, 99, 99, 99, 99]);
1448        let stats = telemetry.snapshot();
1449        assert_eq!(stats.scratch_growths, 1);
1450        assert_eq!((stats.peak_bytes, stats.waves), (7, 13));
1451        assert_eq!(
1452            (stats.enters, stats.submissions, stats.completions),
1453            (13, 13, 13)
1454        );
1455    }
1456
1457    #[test]
1458    fn many_small_requests_share_one_arena_and_one_enter() {
1459        let root = tempfile::tempdir().unwrap();
1460        let path = root.path().join("file");
1461        std::fs::write(&path, vec![42; 1024]).unwrap();
1462        let file = Arc::new(File::open(&path).unwrap());
1463        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1464        for _ in 0..3 {
1465            let mut bytes = vec![[0; 4]; 256];
1466            let mut operations: Vec<_> = bytes
1467                .iter_mut()
1468                .enumerate()
1469                .map(|(i, buffer)| Read {
1470                    file: &file,
1471                    offset: (i * 4) as u64,
1472                    buffer,
1473                })
1474                .collect();
1475            let results = engine.read(&mut operations);
1476            assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1477            assert!(bytes.iter().all(|buffer| buffer == &[42; 4]));
1478        }
1479        let stats = telemetry.snapshot();
1480        assert_eq!(
1481            (stats.waves, stats.enters, stats.scratch_growths),
1482            (3, 3, 1)
1483        );
1484        assert_eq!((stats.submissions, stats.completions), (768, 768));
1485        let results = engine.read_owned(
1486            (0..256)
1487                .map(|i| OwnedRead {
1488                    file: Arc::clone(&file),
1489                    offset: (i * 4) as u64,
1490                    buffer: vec![0; 4],
1491                })
1492                .collect(),
1493        );
1494        assert!(
1495            results
1496                .iter()
1497                .all(|result| result.as_ref().unwrap() == &[42; 4])
1498        );
1499        let stats = telemetry.snapshot();
1500        assert_eq!(
1501            (stats.waves, stats.enters, stats.scratch_growths),
1502            (4, 4, 1)
1503        );
1504        assert_eq!((stats.submissions, stats.completions), (1024, 1024));
1505        assert_eq!(
1506            stats.copied_read_bytes,
1507            3 * 1024,
1508            "the owned cohort adds no copies"
1509        );
1510    }
1511
1512    #[test]
1513    fn eof_relative_reads_observe_external_growth_and_truncation() {
1514        let root = tempfile::tempdir().unwrap();
1515        let path = root.path().join("file");
1516        std::fs::write(&path, b"abc").unwrap();
1517        let (engine, _) = Engine::new(Options::default()).unwrap();
1518        let mut fs = vfsi_local::LocalBackend::new(root.path().to_path_buf(), engine).unwrap();
1519        let file = fs
1520            .open_raw_impl(std::path::Path::new("/file"), libc::O_RDONLY, 0)
1521            .unwrap();
1522        for bytes in [b"abcdefgh".as_slice(), b"x"] {
1523            std::fs::write(&path, bytes).unwrap();
1524            let results = fs
1525                .vread_impl(&[
1526                    ReadOp {
1527                        file: file.clone(),
1528                        offset: VfOffset::At(0),
1529                        length: 10,
1530                    },
1531                    ReadOp {
1532                        file: file.clone(),
1533                        offset: VfOffset::End,
1534                        length: 1,
1535                    },
1536                ])
1537                .unwrap();
1538            assert_eq!(results[0].data, bytes);
1539            assert!(results[0].eof);
1540            assert_eq!(results[1].offset, bytes.len() as u64);
1541            assert!(results[1].eof);
1542            assert!(results[1].data.is_empty());
1543            assert_eq!(
1544                fs.seek_raw_impl(&file, 0, vfsi_core::SeekFrom::Cur)
1545                    .unwrap(),
1546                0
1547            );
1548        }
1549        fs.close_impl(&file).unwrap();
1550    }
1551
1552    #[test]
1553    fn injected_short_writes_complete_contiguous_suffixes_and_short_reads_stop() {
1554        let root = tempfile::tempdir().unwrap();
1555        let path = root.path().join("file");
1556        std::fs::write(&path, b"0000000000").unwrap();
1557        let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1558        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1559        engine.short_completion = Some(2);
1560        assert_eq!(
1561            engine.write(&[Write {
1562                file: &file,
1563                offset: 0,
1564                data: b"abcdefghij"
1565            }])[0]
1566                .as_ref()
1567                .unwrap(),
1568            &10
1569        );
1570        assert_eq!(std::fs::read(&path).unwrap(), b"abcdefghij");
1571        assert_eq!(telemetry.snapshot().waves, 5);
1572        let mut bytes = [99; 10];
1573        assert_eq!(
1574            engine.read(&mut [Read {
1575                file: &file,
1576                offset: 0,
1577                buffer: &mut bytes
1578            }])[0]
1579                .as_ref()
1580                .unwrap(),
1581            &2
1582        );
1583        assert_eq!(bytes, [b'a', b'b', 99, 99, 99, 99, 99, 99, 99, 99]);
1584        assert_eq!(
1585            telemetry.snapshot().submissions,
1586            telemetry.snapshot().completions
1587        );
1588    }
1589
1590    #[test]
1591    fn duplicate_cursor_reads_and_append_remain_ordered() {
1592        let root = tempfile::tempdir().unwrap();
1593        std::fs::write(root.path().join("file"), b"abcdef").unwrap();
1594        let (engine, telemetry) = Engine::new(Options::default()).unwrap();
1595        let mut fs = vfsi_local::LocalBackend::new(root.path().to_path_buf(), engine).unwrap();
1596        let file = fs
1597            .open_raw_impl(std::path::Path::new("/file"), libc::O_RDWR, 0)
1598            .unwrap();
1599        let read = ReadOp {
1600            file: file.clone(),
1601            offset: VfOffset::Cur,
1602            length: 2,
1603        };
1604        let results = fs.vread_impl(&[read.clone(), read]).unwrap();
1605        assert_eq!(results[0].data, b"ab");
1606        assert_eq!(results[1].data, b"cd");
1607        assert_eq!(telemetry.snapshot().submissions, 0);
1608        let positional = ReadOp {
1609            file: file.clone(),
1610            offset: VfOffset::At(0),
1611            length: 2,
1612        };
1613        fs.vread_impl(&[positional]).unwrap();
1614        assert_eq!(
1615            fs.seek_raw_impl(&file, 0, vfsi_core::SeekFrom::Cur)
1616                .unwrap(),
1617            4
1618        );
1619        let append = fs
1620            .open_raw_impl(
1621                std::path::Path::new("/file"),
1622                libc::O_WRONLY | libc::O_APPEND,
1623                0,
1624            )
1625            .unwrap();
1626        fs.vwrite_impl(&[WriteOp::new(&append, VfOffset::At(0), b"xy".as_slice())])
1627            .unwrap();
1628        assert_eq!(
1629            std::fs::read(root.path().join("file")).unwrap(),
1630            b"abcdefxy"
1631        );
1632        fs.close_impl(&file).unwrap();
1633        fs.close_impl(&append).unwrap();
1634    }
1635
1636    #[test]
1637    fn failure_does_not_finish_another_jobs_short_write() {
1638        let root = tempfile::tempdir().unwrap();
1639        let path = root.path().join("file");
1640        std::fs::write(&path, b"000").unwrap();
1641        let writable = Arc::new(File::options().write(true).open(&path).unwrap());
1642        let readonly = Arc::new(File::open(&path).unwrap());
1643        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1644        engine.short_completion = Some(2);
1645        let results = engine.write(&[
1646            Write {
1647                file: &writable,
1648                offset: 0,
1649                data: b"abc",
1650            },
1651            Write {
1652                file: &readonly,
1653                offset: 0,
1654                data: b"xyz",
1655            },
1656        ]);
1657        assert_eq!(*results[0].as_ref().unwrap(), 2);
1658        assert_eq!(
1659            results[1].as_ref().unwrap_err().raw_os_error(),
1660            Some(libc::EBADF)
1661        );
1662        assert_eq!(std::fs::read(path).unwrap(), b"ab0");
1663        let stats = telemetry.snapshot();
1664        assert_eq!(
1665            (stats.waves, stats.submissions, stats.completions),
1666            (1, 2, 2)
1667        );
1668    }
1669
1670    #[test]
1671    fn failure_after_publication_retains_inflight_ownership_without_replay() {
1672        let root = tempfile::tempdir().unwrap();
1673        let path = root.path().join("file");
1674        std::fs::write(&path, b"old").unwrap();
1675        let file = Arc::new(File::options().write(true).open(&path).unwrap());
1676        let weak = Arc::downgrade(&file);
1677        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1678        engine.fail_after_submit = true;
1679        let results = engine.write(&[
1680            Write {
1681                file: &file,
1682                offset: 0,
1683                data: b"ne",
1684            },
1685            Write {
1686                file: &file,
1687                offset: 2,
1688                data: b"w",
1689            },
1690        ]);
1691        assert!(results.iter().all(Result::is_err));
1692        assert!(engine.ring.is_none());
1693        assert_eq!(telemetry.snapshot().submissions, 1);
1694        assert_eq!(telemetry.snapshot().completions, 0);
1695        assert!(
1696            engine.scratch.is_empty(),
1697            "in-flight arena is retained, not reused"
1698        );
1699        assert!(
1700            engine.write(&[Write {
1701                file: &file,
1702                offset: 0,
1703                data: b"bad"
1704            }])[0]
1705                .is_err()
1706        );
1707        assert_eq!(telemetry.snapshot().submissions, 1);
1708        assert_eq!(std::fs::read(&path).unwrap(), b"new");
1709        assert!(
1710            engine.scratch.is_empty(),
1711            "poisoned calls must not grow a new arena"
1712        );
1713        drop(engine);
1714        drop(file);
1715        // This exceptional path deliberately retains the bounded wave:
1716        // dropping the caller/engine must not release kernel-facing storage.
1717        assert!(weak.upgrade().is_some());
1718    }
1719
1720    #[test]
1721    fn poisoned_executor_does_not_submit_or_replay_writes() {
1722        let root = tempfile::tempdir().unwrap();
1723        let path = root.path().join("file");
1724        std::fs::write(&path, b"old").unwrap();
1725        let file = Arc::new(File::options().write(true).open(&path).unwrap());
1726        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1727        engine.ring.take();
1728        assert!(
1729            engine.write(&[Write {
1730                file: &file,
1731                offset: 0,
1732                data: b"new"
1733            }])[0]
1734                .is_err()
1735        );
1736        assert_eq!(std::fs::read(&path).unwrap(), b"old");
1737        assert_eq!(telemetry.snapshot().submissions, 0);
1738    }
1739}