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#[derive(Clone, Copy, Debug, Default)]
32pub struct Stats {
33 pub syscall_reads: u64,
35 pub syscall_writes: u64,
37 pub cache_probes: u64,
39 pub cache_hits: u64,
41 pub cache_misses: u64,
43 pub waves: u64,
44 pub submissions: u64,
45 pub completions: u64,
46 pub peak_bytes: u64,
47 pub enters: u64,
49 pub scratch_growths: u64,
51 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 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 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 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 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 let mut scratch = std::mem::take(&mut self.scratch);
240 let scratch_len = scratch.len();
241 let scratch_ptr = scratch.as_mut_ptr();
244 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 probe = false;
679 count
680 }
681 Err(error) if error.raw_os_error() == Some(libc::EAGAIN) => {
682 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 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 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 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 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}