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