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                    .queue_depth(std::num::NonZeroU32::new(3).unwrap())
812                    .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().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().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) = Engine::new(Options::default().syscall_writes(true)).unwrap();
1024        assert!(engine.ordered());
1025        let operations = [
1026            Write {
1027                file: &write,
1028                offset: 0,
1029                data: b"A",
1030            },
1031            Write {
1032                file: &read,
1033                offset: 1,
1034                data: b"B",
1035            },
1036            Write {
1037                file: &write,
1038                offset: 2,
1039                data: b"C",
1040            },
1041        ];
1042        let results = engine.write(&operations);
1043        assert_eq!(*results[0].as_ref().unwrap(), 1);
1044        assert_eq!(
1045            results[1].as_ref().unwrap_err().raw_os_error(),
1046            Some(libc::EBADF)
1047        );
1048        assert_eq!(*results[2].as_ref().unwrap(), 0);
1049        assert_eq!(std::fs::read(&path).unwrap(), b"Ald");
1050        let stats = telemetry.snapshot();
1051        assert_eq!(
1052            (stats.syscall_writes, stats.waves, stats.submissions),
1053            (2, 0, 0)
1054        );
1055        engine.ring.take();
1056        assert!(engine.write(&operations).iter().all(Result::is_err));
1057        assert_eq!(std::fs::read(path).unwrap(), b"Ald");
1058        assert_eq!(telemetry.snapshot().syscall_writes, 2);
1059    }
1060
1061    #[test]
1062    fn cached_reads_preserve_partial_prefixes_and_batch_misses_without_repeated_probes() {
1063        for (error, limit, hits, probes, misses, submissions) in [
1064            (None, usize::MAX, 2, 2, 0, 0),
1065            (None, 3, 0, 1, 2, 2),
1066            (Some(libc::EAGAIN), usize::MAX, 0, 1, 2, 2),
1067            (Some(libc::EOPNOTSUPP), usize::MAX, 0, 1, 2, 2),
1068            (Some(libc::ENOSYS), usize::MAX, 0, 1, 2, 2),
1069        ] {
1070            let root = tempfile::tempdir().unwrap();
1071            let path = root.path().join("file");
1072            std::fs::write(&path, b"abcdefgh").unwrap();
1073            let file = Arc::new(File::open(path).unwrap());
1074            let (mut engine, telemetry) =
1075                Engine::new(Options::default().cached_reads(true)).unwrap();
1076            engine.cache_read_limit = Some(limit);
1077            engine.cache_read_error = error.map(|errno| (0, errno));
1078            let mut first = [99; 4];
1079            let mut second = [99; 4];
1080            for round in 1..=2 {
1081                let results = engine.read(&mut [
1082                    Read {
1083                        file: &file,
1084                        offset: 0,
1085                        buffer: &mut first,
1086                    },
1087                    Read {
1088                        file: &file,
1089                        offset: 4,
1090                        buffer: &mut second,
1091                    },
1092                ]);
1093                assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1094                assert_eq!((first, second), (*b"abcd", *b"efgh"));
1095                let stats = telemetry.snapshot();
1096                let unsupported = matches!(error, Some(libc::EOPNOTSUPP | libc::ENOSYS));
1097                let probes_rounds = if unsupported { 1 } else { round };
1098                assert_eq!(stats.cache_probes, probes * probes_rounds);
1099                assert_eq!(stats.cache_hits, hits * round);
1100                assert_eq!(stats.syscall_reads, 0);
1101                assert_eq!(stats.cache_misses, misses * probes_rounds);
1102                assert_eq!(stats.submissions, submissions * round);
1103                assert_eq!(stats.completions, stats.submissions);
1104                assert_eq!(engine.nowait_supported, !unsupported);
1105            }
1106        }
1107    }
1108
1109    #[test]
1110    fn warm_cohorts_reprobe_and_keep_original_indices_when_the_next_cohort_is_cold() {
1111        let root = tempfile::tempdir().unwrap();
1112        let path = root.path().join("file");
1113        let expected: Vec<u8> = (0..128).collect();
1114        std::fs::write(&path, &expected).unwrap();
1115        let file = Arc::new(File::open(path).unwrap());
1116        let (mut engine, telemetry) = Engine::new(Options::default().cached_reads(true)).unwrap();
1117        engine.cache_read_limit = Some(usize::MAX);
1118        engine.cache_read_error = Some((2, libc::EAGAIN));
1119        let mut buffers = [[99; 4]; 32];
1120        let mut operations: Vec<_> = buffers
1121            .iter_mut()
1122            .enumerate()
1123            .map(|(i, buffer)| Read {
1124                file: &file,
1125                offset: (i * 4) as u64,
1126                buffer,
1127            })
1128            .collect();
1129        let results = engine.read(&mut operations);
1130        assert_eq!(results.len(), 32);
1131        assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1132        assert_eq!(buffers.concat(), expected);
1133        let stats = telemetry.snapshot();
1134        assert_eq!(
1135            (stats.cache_probes, stats.cache_hits, stats.syscall_reads),
1136            (3, 2, 14)
1137        );
1138        assert_eq!(
1139            (
1140                stats.cache_misses,
1141                stats.waves,
1142                stats.submissions,
1143                stats.completions
1144            ),
1145            (16, 1, 16, 16)
1146        );
1147    }
1148
1149    #[test]
1150    fn cached_reads_keep_eof_tail_and_stop_after_a_real_error() {
1151        let root = tempfile::tempdir().unwrap();
1152        let path = root.path().join("file");
1153        std::fs::write(&path, b"abc").unwrap();
1154        let file = Arc::new(File::open(&path).unwrap());
1155        let writeonly = Arc::new(File::options().write(true).open(path).unwrap());
1156        let (mut engine, telemetry) = Engine::new(Options::default().cached_reads(true)).unwrap();
1157        engine.cache_read_limit = Some(usize::MAX);
1158        let mut bytes = [99; 5];
1159        let results = engine.read(&mut [Read {
1160            file: &file,
1161            offset: 0,
1162            buffer: &mut bytes,
1163        }]);
1164        assert_eq!(*results[0].as_ref().unwrap(), 3);
1165        assert_eq!(bytes, [b'a', b'b', b'c', 99, 99]);
1166        assert_eq!(
1167            telemetry.snapshot().submissions,
1168            1,
1169            "partial cache hits must probe EOF"
1170        );
1171        let before = telemetry.snapshot().cache_probes;
1172        assert_eq!(
1173            *engine.read(&mut [Read {
1174                file: &writeonly,
1175                offset: 0,
1176                buffer: &mut [],
1177            }])[0]
1178                .as_ref()
1179                .unwrap(),
1180            0
1181        );
1182        assert_eq!(
1183            telemetry.snapshot().cache_probes,
1184            before,
1185            "empty reads do not issue syscalls"
1186        );
1187        engine.cache_read_limit = Some(0);
1188        let results = engine.read(&mut [Read {
1189            file: &file,
1190            offset: 0,
1191            buffer: &mut bytes[..3],
1192        }]);
1193        assert_eq!(
1194            *results[0].as_ref().unwrap(),
1195            3,
1196            "a zero NOWAIT reply cannot establish EOF"
1197        );
1198        assert_eq!(&bytes[..3], b"abc");
1199        engine.cache_read_limit = Some(usize::MAX);
1200        let mut first = [99; 3];
1201        let mut invalid = [99; 3];
1202        let mut untouched = [99; 3];
1203        let results = engine.read(&mut [
1204            Read {
1205                file: &file,
1206                offset: 0,
1207                buffer: &mut first,
1208            },
1209            Read {
1210                file: &writeonly,
1211                offset: 0,
1212                buffer: &mut invalid,
1213            },
1214            Read {
1215                file: &file,
1216                offset: 0,
1217                buffer: &mut untouched,
1218            },
1219        ]);
1220        assert_eq!(*results[0].as_ref().unwrap(), 3);
1221        assert_eq!(
1222            results[1].as_ref().unwrap_err().raw_os_error(),
1223            Some(libc::EBADF)
1224        );
1225        assert_eq!(*results[2].as_ref().unwrap(), 0);
1226        assert_eq!(first, *b"abc");
1227        assert_eq!((invalid, untouched), ([99; 3], [99; 3]));
1228        assert_eq!(telemetry.snapshot().submissions, 2);
1229        engine.ring.take();
1230        assert!(
1231            engine.read(&mut [Read {
1232                file: &file,
1233                offset: 0,
1234                buffer: &mut untouched
1235            }])[0]
1236                .is_err()
1237        );
1238        assert_eq!(
1239            untouched, [99; 3],
1240            "poisoned executors must not bypass the ring through cache hits"
1241        );
1242    }
1243
1244    #[test]
1245    fn cooperative_ring_can_move_between_threads_after_draining_each_call() {
1246        let root = tempfile::tempdir().unwrap();
1247        let path = root.path().join("file");
1248        std::fs::write(&path, b"old").unwrap();
1249        let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1250        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1251        let (mut engine, file) = std::thread::spawn(move || {
1252            assert_eq!(
1253                *engine.write(&[Write {
1254                    file: &file,
1255                    offset: 0,
1256                    data: b"new"
1257                }])[0]
1258                    .as_ref()
1259                    .unwrap(),
1260                3
1261            );
1262            (engine, file)
1263        })
1264        .join()
1265        .unwrap();
1266        let mut bytes = [0; 3];
1267        assert_eq!(
1268            *engine.read(&mut [Read {
1269                file: &file,
1270                offset: 0,
1271                buffer: &mut bytes
1272            }])[0]
1273                .as_ref()
1274                .unwrap(),
1275            3
1276        );
1277        assert_eq!(bytes, *b"new");
1278        let stats = telemetry.snapshot();
1279        assert_eq!(
1280            (stats.waves, stats.submissions, stats.completions),
1281            (2, 2, 2)
1282        );
1283    }
1284
1285    #[test]
1286    fn merged_short_writes_preserve_each_requests_contiguous_progress() {
1287        for short in [1, 3, 4, 6] {
1288            let root = tempfile::tempdir().unwrap();
1289            let path = root.path().join("file");
1290            std::fs::write(&path, b"00000000").unwrap();
1291            let file = Arc::new(File::options().write(true).open(&path).unwrap());
1292            let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1293            engine.short_completion = Some(short);
1294            let results = engine.write(&[
1295                Write {
1296                    file: &file,
1297                    offset: 0,
1298                    data: b"abc",
1299                },
1300                Write {
1301                    file: &file,
1302                    offset: 3,
1303                    data: b"defgh",
1304                },
1305            ]);
1306            assert_eq!(*results[0].as_ref().unwrap(), 3);
1307            assert_eq!(*results[1].as_ref().unwrap(), 5);
1308            assert_eq!(std::fs::read(&path).unwrap(), b"abcdefgh");
1309            let waves = 8usize.div_ceil(short) as u64;
1310            let stats = telemetry.snapshot();
1311            assert_eq!(
1312                (stats.waves, stats.submissions, stats.completions),
1313                (waves, waves, waves)
1314            );
1315        }
1316    }
1317
1318    #[test]
1319    fn merged_short_write_is_not_replayed_when_another_completion_fails() {
1320        for short in [1, 3, 4, 6] {
1321            let root = tempfile::tempdir().unwrap();
1322            let path = root.path().join("file");
1323            std::fs::write(&path, b"00000000").unwrap();
1324            let file = Arc::new(File::options().write(true).open(&path).unwrap());
1325            let readonly = Arc::new(File::open(&path).unwrap());
1326            let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1327            engine.short_completion = Some(short);
1328            let results = engine.write(&[
1329                Write {
1330                    file: &file,
1331                    offset: 0,
1332                    data: b"abc",
1333                },
1334                Write {
1335                    file: &file,
1336                    offset: 3,
1337                    data: b"defgh",
1338                },
1339                Write {
1340                    file: &readonly,
1341                    offset: 0,
1342                    data: b"bad",
1343                },
1344            ]);
1345            assert_eq!(*results[0].as_ref().unwrap(), short.min(3));
1346            assert_eq!(*results[1].as_ref().unwrap(), short.saturating_sub(3));
1347            assert_eq!(
1348                results[2].as_ref().unwrap_err().raw_os_error(),
1349                Some(libc::EBADF)
1350            );
1351            let mut expected = *b"00000000";
1352            expected[..short].copy_from_slice(&b"abcdefgh"[..short]);
1353            assert_eq!(std::fs::read(&path).unwrap(), expected);
1354            let stats = telemetry.snapshot();
1355            assert_eq!(
1356                (stats.waves, stats.submissions, stats.completions),
1357                (1, 2, 2)
1358            );
1359        }
1360    }
1361
1362    #[test]
1363    fn merged_zero_progress_and_failed_writes_never_retry_the_group() {
1364        for zero_progress in [false, true] {
1365            let root = tempfile::tempdir().unwrap();
1366            let path = root.path().join("file");
1367            std::fs::write(&path, b"000000").unwrap();
1368            let file = Arc::new(
1369                File::options()
1370                    .read(true)
1371                    .write(zero_progress)
1372                    .open(&path)
1373                    .unwrap(),
1374            );
1375            let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1376            engine.short_completion = zero_progress.then_some(0);
1377            let results = engine.write(&[
1378                Write {
1379                    file: &file,
1380                    offset: 0,
1381                    data: b"abc",
1382                },
1383                Write {
1384                    file: &file,
1385                    offset: 3,
1386                    data: b"DEF",
1387                },
1388            ]);
1389            let errno = if zero_progress {
1390                libc::EIO
1391            } else {
1392                libc::EBADF
1393            };
1394            assert!(
1395                results
1396                    .iter()
1397                    .all(|result| result.as_ref().unwrap_err().raw_os_error() == Some(errno))
1398            );
1399            assert_eq!(std::fs::read(path).unwrap(), b"000000");
1400            let stats = telemetry.snapshot();
1401            assert_eq!(
1402                (stats.waves, stats.submissions, stats.completions),
1403                (1, 1, 1)
1404            );
1405        }
1406    }
1407
1408    #[test]
1409    fn waves_reuse_bounded_storage_and_wait_once_for_all_completions() {
1410        let root = tempfile::tempdir().unwrap();
1411        let path = root.path().join("file");
1412        std::fs::write(&path, b"0123456789").unwrap();
1413        let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1414        let (mut engine, telemetry) = Engine::new(
1415            Options::default().max_batch_bytes(std::num::NonZeroUsize::new(7).unwrap()),
1416        )
1417        .unwrap();
1418        for value in *b"abc" {
1419            let bytes = [value; 10];
1420            let results = engine.write(&[Write {
1421                file: &file,
1422                offset: 0,
1423                data: &bytes,
1424            }]);
1425            assert_eq!(*results[0].as_ref().unwrap(), 10);
1426            let mut actual = [0; 10];
1427            let results = engine.read(&mut [Read {
1428                file: &file,
1429                offset: 0,
1430                buffer: &mut actual,
1431            }]);
1432            assert_eq!(*results[0].as_ref().unwrap(), 10);
1433            assert_eq!(actual, bytes);
1434            assert!(engine.scratch.capacity() <= 7);
1435        }
1436        // Recycled storage must not leak stale bytes past a short/EOF read.
1437        let mut tail = [99; 7];
1438        let results = engine.read(&mut [Read {
1439            file: &file,
1440            offset: 8,
1441            buffer: &mut tail,
1442        }]);
1443        assert_eq!(*results[0].as_ref().unwrap(), 2);
1444        assert_eq!(tail, [b'c', b'c', 99, 99, 99, 99, 99]);
1445        let stats = telemetry.snapshot();
1446        assert_eq!(stats.scratch_growths, 1);
1447        assert_eq!((stats.peak_bytes, stats.waves), (7, 13));
1448        assert_eq!(
1449            (stats.enters, stats.submissions, stats.completions),
1450            (13, 13, 13)
1451        );
1452    }
1453
1454    #[test]
1455    fn many_small_requests_share_one_arena_and_one_enter() {
1456        let root = tempfile::tempdir().unwrap();
1457        let path = root.path().join("file");
1458        std::fs::write(&path, vec![42; 1024]).unwrap();
1459        let file = Arc::new(File::open(&path).unwrap());
1460        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1461        for _ in 0..3 {
1462            let mut bytes = vec![[0; 4]; 256];
1463            let mut operations: Vec<_> = bytes
1464                .iter_mut()
1465                .enumerate()
1466                .map(|(i, buffer)| Read {
1467                    file: &file,
1468                    offset: (i * 4) as u64,
1469                    buffer,
1470                })
1471                .collect();
1472            let results = engine.read(&mut operations);
1473            assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1474            assert!(bytes.iter().all(|buffer| buffer == &[42; 4]));
1475        }
1476        let stats = telemetry.snapshot();
1477        assert_eq!(
1478            (stats.waves, stats.enters, stats.scratch_growths),
1479            (3, 3, 1)
1480        );
1481        assert_eq!((stats.submissions, stats.completions), (768, 768));
1482        let results = engine.read_owned(
1483            (0..256)
1484                .map(|i| OwnedRead {
1485                    file: Arc::clone(&file),
1486                    offset: (i * 4) as u64,
1487                    buffer: vec![0; 4],
1488                })
1489                .collect(),
1490        );
1491        assert!(
1492            results
1493                .iter()
1494                .all(|result| result.as_ref().unwrap() == &[42; 4])
1495        );
1496        let stats = telemetry.snapshot();
1497        assert_eq!(
1498            (stats.waves, stats.enters, stats.scratch_growths),
1499            (4, 4, 1)
1500        );
1501        assert_eq!((stats.submissions, stats.completions), (1024, 1024));
1502        assert_eq!(
1503            stats.copied_read_bytes,
1504            3 * 1024,
1505            "the owned cohort adds no copies"
1506        );
1507    }
1508
1509    #[test]
1510    fn eof_relative_reads_observe_external_growth_and_truncation() {
1511        let root = tempfile::tempdir().unwrap();
1512        let path = root.path().join("file");
1513        std::fs::write(&path, b"abc").unwrap();
1514        let (engine, _) = Engine::new(Options::default()).unwrap();
1515        let mut fs = vfsi_local::LocalBackend::new(root.path().to_path_buf(), engine).unwrap();
1516        let file = fs
1517            .open_raw_impl(std::path::Path::new("/file"), libc::O_RDONLY, 0)
1518            .unwrap();
1519        for bytes in [b"abcdefgh".as_slice(), b"x"] {
1520            std::fs::write(&path, bytes).unwrap();
1521            let results = fs
1522                .vread_impl(&[
1523                    ReadOp {
1524                        file: file.clone(),
1525                        offset: VfOffset::At(0),
1526                        length: 10,
1527                    },
1528                    ReadOp {
1529                        file: file.clone(),
1530                        offset: VfOffset::End,
1531                        length: 1,
1532                    },
1533                ])
1534                .unwrap();
1535            assert_eq!(results[0].data, bytes);
1536            assert!(results[0].eof);
1537            assert_eq!(results[1].offset, bytes.len() as u64);
1538            assert!(results[1].eof);
1539            assert!(results[1].data.is_empty());
1540            assert_eq!(
1541                fs.seek_raw_impl(&file, 0, vfsi_core::SeekFrom::Cur)
1542                    .unwrap(),
1543                0
1544            );
1545        }
1546        fs.close_impl(&file).unwrap();
1547    }
1548
1549    #[test]
1550    fn injected_short_writes_complete_contiguous_suffixes_and_short_reads_stop() {
1551        let root = tempfile::tempdir().unwrap();
1552        let path = root.path().join("file");
1553        std::fs::write(&path, b"0000000000").unwrap();
1554        let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1555        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1556        engine.short_completion = Some(2);
1557        assert_eq!(
1558            engine.write(&[Write {
1559                file: &file,
1560                offset: 0,
1561                data: b"abcdefghij"
1562            }])[0]
1563                .as_ref()
1564                .unwrap(),
1565            &10
1566        );
1567        assert_eq!(std::fs::read(&path).unwrap(), b"abcdefghij");
1568        assert_eq!(telemetry.snapshot().waves, 5);
1569        let mut bytes = [99; 10];
1570        assert_eq!(
1571            engine.read(&mut [Read {
1572                file: &file,
1573                offset: 0,
1574                buffer: &mut bytes
1575            }])[0]
1576                .as_ref()
1577                .unwrap(),
1578            &2
1579        );
1580        assert_eq!(bytes, [b'a', b'b', 99, 99, 99, 99, 99, 99, 99, 99]);
1581        assert_eq!(
1582            telemetry.snapshot().submissions,
1583            telemetry.snapshot().completions
1584        );
1585    }
1586
1587    #[test]
1588    fn duplicate_cursor_reads_and_append_remain_ordered() {
1589        let root = tempfile::tempdir().unwrap();
1590        std::fs::write(root.path().join("file"), b"abcdef").unwrap();
1591        let (engine, telemetry) = Engine::new(Options::default()).unwrap();
1592        let mut fs = vfsi_local::LocalBackend::new(root.path().to_path_buf(), engine).unwrap();
1593        let file = fs
1594            .open_raw_impl(std::path::Path::new("/file"), libc::O_RDWR, 0)
1595            .unwrap();
1596        let read = ReadOp {
1597            file: file.clone(),
1598            offset: VfOffset::Cur,
1599            length: 2,
1600        };
1601        let results = fs.vread_impl(&[read.clone(), read]).unwrap();
1602        assert_eq!(results[0].data, b"ab");
1603        assert_eq!(results[1].data, b"cd");
1604        assert_eq!(telemetry.snapshot().submissions, 0);
1605        let positional = ReadOp {
1606            file: file.clone(),
1607            offset: VfOffset::At(0),
1608            length: 2,
1609        };
1610        fs.vread_impl(&[positional]).unwrap();
1611        assert_eq!(
1612            fs.seek_raw_impl(&file, 0, vfsi_core::SeekFrom::Cur)
1613                .unwrap(),
1614            4
1615        );
1616        let append = fs
1617            .open_raw_impl(
1618                std::path::Path::new("/file"),
1619                libc::O_WRONLY | libc::O_APPEND,
1620                0,
1621            )
1622            .unwrap();
1623        fs.vwrite_impl(&[WriteOp::new(&append, VfOffset::At(0), b"xy".as_slice())])
1624            .unwrap();
1625        assert_eq!(
1626            std::fs::read(root.path().join("file")).unwrap(),
1627            b"abcdefxy"
1628        );
1629        fs.close_impl(&file).unwrap();
1630        fs.close_impl(&append).unwrap();
1631    }
1632
1633    #[test]
1634    fn failure_does_not_finish_another_jobs_short_write() {
1635        let root = tempfile::tempdir().unwrap();
1636        let path = root.path().join("file");
1637        std::fs::write(&path, b"000").unwrap();
1638        let writable = Arc::new(File::options().write(true).open(&path).unwrap());
1639        let readonly = Arc::new(File::open(&path).unwrap());
1640        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1641        engine.short_completion = Some(2);
1642        let results = engine.write(&[
1643            Write {
1644                file: &writable,
1645                offset: 0,
1646                data: b"abc",
1647            },
1648            Write {
1649                file: &readonly,
1650                offset: 0,
1651                data: b"xyz",
1652            },
1653        ]);
1654        assert_eq!(*results[0].as_ref().unwrap(), 2);
1655        assert_eq!(
1656            results[1].as_ref().unwrap_err().raw_os_error(),
1657            Some(libc::EBADF)
1658        );
1659        assert_eq!(std::fs::read(path).unwrap(), b"ab0");
1660        let stats = telemetry.snapshot();
1661        assert_eq!(
1662            (stats.waves, stats.submissions, stats.completions),
1663            (1, 2, 2)
1664        );
1665    }
1666
1667    #[test]
1668    fn failure_after_publication_retains_inflight_ownership_without_replay() {
1669        let root = tempfile::tempdir().unwrap();
1670        let path = root.path().join("file");
1671        std::fs::write(&path, b"old").unwrap();
1672        let file = Arc::new(File::options().write(true).open(&path).unwrap());
1673        let weak = Arc::downgrade(&file);
1674        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1675        engine.fail_after_submit = true;
1676        let results = engine.write(&[
1677            Write {
1678                file: &file,
1679                offset: 0,
1680                data: b"ne",
1681            },
1682            Write {
1683                file: &file,
1684                offset: 2,
1685                data: b"w",
1686            },
1687        ]);
1688        assert!(results.iter().all(Result::is_err));
1689        assert!(engine.ring.is_none());
1690        assert_eq!(telemetry.snapshot().submissions, 1);
1691        assert_eq!(telemetry.snapshot().completions, 0);
1692        assert!(
1693            engine.scratch.is_empty(),
1694            "in-flight arena is retained, not reused"
1695        );
1696        assert!(
1697            engine.write(&[Write {
1698                file: &file,
1699                offset: 0,
1700                data: b"bad"
1701            }])[0]
1702                .is_err()
1703        );
1704        assert_eq!(telemetry.snapshot().submissions, 1);
1705        assert_eq!(std::fs::read(&path).unwrap(), b"new");
1706        assert!(
1707            engine.scratch.is_empty(),
1708            "poisoned calls must not grow a new arena"
1709        );
1710        drop(engine);
1711        drop(file);
1712        // This exceptional path deliberately retains the bounded wave:
1713        // dropping the caller/engine must not release kernel-facing storage.
1714        assert!(weak.upgrade().is_some());
1715    }
1716
1717    #[test]
1718    fn poisoned_executor_does_not_submit_or_replay_writes() {
1719        let root = tempfile::tempdir().unwrap();
1720        let path = root.path().join("file");
1721        std::fs::write(&path, b"old").unwrap();
1722        let file = Arc::new(File::options().write(true).open(&path).unwrap());
1723        let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1724        engine.ring.take();
1725        assert!(
1726            engine.write(&[Write {
1727                file: &file,
1728                offset: 0,
1729                data: b"new"
1730            }])[0]
1731                .is_err()
1732        );
1733        assert_eq!(std::fs::read(&path).unwrap(), b"old");
1734        assert_eq!(telemetry.snapshot().submissions, 0);
1735    }
1736}