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 .with_queue_depth(std::num::NonZeroU32::new(3).unwrap())
812 .with_max_batch_bytes(std::num::NonZeroUsize::new(16).unwrap()),
813 )
814 .unwrap();
815 engine.short_completion = short;
816 let mut capacities = None;
817 for _ in 0..3 {
818 let buffer = vec![99; 8];
819 let pointer = buffer.as_ptr();
820 let results = engine.read_owned(vec![
821 OwnedRead {
822 file: Arc::clone(&file),
823 offset: 1,
824 buffer,
825 },
826 OwnedRead {
827 file: Arc::clone(&file),
828 offset: 8,
829 buffer: vec![99; 4],
830 },
831 OwnedRead {
832 file: Arc::clone(&file),
833 offset: 0,
834 buffer: Vec::new(),
835 },
836 ]);
837 let expected = match short {
838 None => b"12345678".as_slice(),
839 Some(2) => b"12",
840 Some(0) => b"",
841 _ => unreachable!(),
842 };
843 assert_eq!(results[0].as_ref().unwrap(), expected);
844 assert_eq!(
845 results[0].as_ref().unwrap().as_ptr(),
846 pointer,
847 "the final allocation must be returned unchanged"
848 );
849 let second = results[1].as_ref().unwrap();
852 assert!(b"89".starts_with(second));
853 if short.is_none() {
854 assert_eq!(second, b"89");
855 }
856 if short == Some(0) {
857 assert!(second.is_empty());
858 }
859 assert!(results[2].as_ref().unwrap().is_empty());
860 let current = (
861 engine.progress.capacity(),
862 engine.done.capacity(),
863 engine.errors.capacity(),
864 engine.groups.capacity(),
865 engine.entries.capacity(),
866 engine.results.capacity(),
867 );
868 if let Some(previous) = capacities {
869 assert_eq!(current, previous);
870 }
871 capacities = Some(current);
872 assert!(engine.progress.capacity() >= 3);
873 }
874 let stats = telemetry.snapshot();
875 assert_eq!((stats.copied_read_bytes, stats.scratch_growths), (0, 0));
876 assert_eq!(stats.submissions, stats.completions);
877 assert!(stats.peak_bytes <= 16);
878 assert!(engine.scratch.is_empty());
879 let mut borrowed = [99; 2];
880 engine.short_completion = None;
881 assert_eq!(
882 *engine.read(&mut [Read {
883 file: &file,
884 offset: 2,
885 buffer: &mut borrowed
886 }])[0]
887 .as_ref()
888 .unwrap(),
889 2
890 );
891 assert_eq!(borrowed, *b"23");
892 assert_eq!(telemetry.snapshot().copied_read_bytes, 2);
893 }
894 }
895
896 #[test]
897 fn owned_cohorts_larger_than_the_window_keep_bounded_scratch_then_return_to_direct_reads() {
898 let root = tempfile::tempdir().unwrap();
899 let path = root.path().join("file");
900 std::fs::write(&path, b"abcdefgh").unwrap();
901 let file = Arc::new(File::open(path).unwrap());
902 let (mut engine, telemetry) = Engine::new(
903 Options::default().with_max_batch_bytes(std::num::NonZeroUsize::new(3).unwrap()),
904 )
905 .unwrap();
906 let results = engine.read_owned(vec![OwnedRead {
907 file: Arc::clone(&file),
908 offset: 0,
909 buffer: vec![99; 8],
910 }]);
911 assert_eq!(results[0].as_ref().unwrap(), b"abcdefgh");
912 let stats = telemetry.snapshot();
913 assert_eq!(
914 (stats.waves, stats.copied_read_bytes, stats.peak_bytes),
915 (3, 8, 3)
916 );
917 let results = engine.read_owned(vec![OwnedRead {
918 file,
919 offset: 2,
920 buffer: vec![99; 3],
921 }]);
922 assert_eq!(results[0].as_ref().unwrap(), b"cde");
923 assert_eq!(telemetry.snapshot().copied_read_bytes, 8);
924 assert_eq!(telemetry.snapshot().scratch_growths, 1);
925 }
926
927 #[test]
928 fn owned_read_errors_drain_a_wave_stop_unsent_work_and_allow_reuse() {
929 let root = tempfile::tempdir().unwrap();
930 let path = root.path().join("file");
931 std::fs::write(&path, b"abcd").unwrap();
932 let file = Arc::new(File::open(&path).unwrap());
933 let writeonly = Arc::new(File::options().write(true).open(path).unwrap());
934 let (mut engine, telemetry) =
935 Engine::new(Options::default().with_queue_depth(std::num::NonZeroU32::new(2).unwrap()))
936 .unwrap();
937 let results = engine.read_owned(vec![
938 OwnedRead {
939 file: Arc::clone(&file),
940 offset: 0,
941 buffer: vec![99; 4],
942 },
943 OwnedRead {
944 file: writeonly,
945 offset: 0,
946 buffer: vec![99; 4],
947 },
948 OwnedRead {
949 file: Arc::clone(&file),
950 offset: 0,
951 buffer: vec![99; 4],
952 },
953 ]);
954 assert_eq!(results[0].as_ref().unwrap(), b"abcd");
955 assert_eq!(
956 results[1].as_ref().unwrap_err().raw_os_error(),
957 Some(libc::EBADF)
958 );
959 assert!(results[2].as_ref().unwrap().is_empty());
960 assert_eq!(
961 (
962 telemetry.snapshot().submissions,
963 telemetry.snapshot().completions
964 ),
965 (2, 2)
966 );
967 assert!(
968 engine.progress.is_empty(),
969 "oversized cohort metadata must not be retained"
970 );
971 let results = engine.read_owned(vec![OwnedRead {
972 file,
973 offset: 1,
974 buffer: vec![0; 3],
975 }]);
976 assert_eq!(results[0].as_ref().unwrap(), b"bcd");
977 assert_eq!(telemetry.snapshot().copied_read_bytes, 0);
978 }
979
980 #[test]
981 fn owned_reads_retain_inflight_allocations_and_descriptors_after_publication_failure() {
982 let root = tempfile::tempdir().unwrap();
983 let path = root.path().join("file");
984 std::fs::write(&path, b"abcd").unwrap();
985 let file = Arc::new(File::open(path).unwrap());
986 let weak = Arc::downgrade(&file);
987 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
988 engine.fail_after_submit = true;
989 let results = engine.read_owned(vec![OwnedRead {
990 file,
991 offset: 0,
992 buffer: vec![99; 4],
993 }]);
994 assert_eq!(
995 results[0].as_ref().unwrap_err().raw_os_error(),
996 Some(libc::EIO)
997 );
998 assert!(engine.ring.is_none());
999 assert!(
1000 weak.upgrade().is_some(),
1001 "the in-flight descriptor is retained with its owned destination"
1002 );
1003 let before = telemetry.snapshot().submissions;
1004 assert!(
1005 engine.read_owned(vec![OwnedRead {
1006 file: weak.upgrade().unwrap(),
1007 offset: 0,
1008 buffer: vec![0; 4]
1009 }])[0]
1010 .is_err()
1011 );
1012 assert_eq!(telemetry.snapshot().submissions, before);
1013 assert_eq!(telemetry.snapshot().copied_read_bytes, 0);
1014 }
1015
1016 #[test]
1017 fn syscall_writes_stop_after_errors_and_cannot_bypass_poisoning() {
1018 let root = tempfile::tempdir().unwrap();
1019 let path = root.path().join("file");
1020 std::fs::write(&path, b"old").unwrap();
1021 let write = Arc::new(File::options().write(true).open(&path).unwrap());
1022 let read = Arc::new(File::open(&path).unwrap());
1023 let (mut engine, telemetry) =
1024 Engine::new(Options::default().with_syscall_writes(true)).unwrap();
1025 assert!(engine.ordered());
1026 let operations = [
1027 Write {
1028 file: &write,
1029 offset: 0,
1030 data: b"A",
1031 },
1032 Write {
1033 file: &read,
1034 offset: 1,
1035 data: b"B",
1036 },
1037 Write {
1038 file: &write,
1039 offset: 2,
1040 data: b"C",
1041 },
1042 ];
1043 let results = engine.write(&operations);
1044 assert_eq!(*results[0].as_ref().unwrap(), 1);
1045 assert_eq!(
1046 results[1].as_ref().unwrap_err().raw_os_error(),
1047 Some(libc::EBADF)
1048 );
1049 assert_eq!(*results[2].as_ref().unwrap(), 0);
1050 assert_eq!(std::fs::read(&path).unwrap(), b"Ald");
1051 let stats = telemetry.snapshot();
1052 assert_eq!(
1053 (stats.syscall_writes, stats.waves, stats.submissions),
1054 (2, 0, 0)
1055 );
1056 engine.ring.take();
1057 assert!(engine.write(&operations).iter().all(Result::is_err));
1058 assert_eq!(std::fs::read(path).unwrap(), b"Ald");
1059 assert_eq!(telemetry.snapshot().syscall_writes, 2);
1060 }
1061
1062 #[test]
1063 fn cached_reads_preserve_partial_prefixes_and_batch_misses_without_repeated_probes() {
1064 for (error, limit, hits, probes, misses, submissions) in [
1065 (None, usize::MAX, 2, 2, 0, 0),
1066 (None, 3, 0, 1, 2, 2),
1067 (Some(libc::EAGAIN), usize::MAX, 0, 1, 2, 2),
1068 (Some(libc::EOPNOTSUPP), usize::MAX, 0, 1, 2, 2),
1069 (Some(libc::ENOSYS), usize::MAX, 0, 1, 2, 2),
1070 ] {
1071 let root = tempfile::tempdir().unwrap();
1072 let path = root.path().join("file");
1073 std::fs::write(&path, b"abcdefgh").unwrap();
1074 let file = Arc::new(File::open(path).unwrap());
1075 let (mut engine, telemetry) =
1076 Engine::new(Options::default().with_cached_reads(true)).unwrap();
1077 engine.cache_read_limit = Some(limit);
1078 engine.cache_read_error = error.map(|errno| (0, errno));
1079 let mut first = [99; 4];
1080 let mut second = [99; 4];
1081 for round in 1..=2 {
1082 let results = engine.read(&mut [
1083 Read {
1084 file: &file,
1085 offset: 0,
1086 buffer: &mut first,
1087 },
1088 Read {
1089 file: &file,
1090 offset: 4,
1091 buffer: &mut second,
1092 },
1093 ]);
1094 assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1095 assert_eq!((first, second), (*b"abcd", *b"efgh"));
1096 let stats = telemetry.snapshot();
1097 let unsupported = matches!(error, Some(libc::EOPNOTSUPP | libc::ENOSYS));
1098 let probes_rounds = if unsupported { 1 } else { round };
1099 assert_eq!(stats.cache_probes, probes * probes_rounds);
1100 assert_eq!(stats.cache_hits, hits * round);
1101 assert_eq!(stats.syscall_reads, 0);
1102 assert_eq!(stats.cache_misses, misses * probes_rounds);
1103 assert_eq!(stats.submissions, submissions * round);
1104 assert_eq!(stats.completions, stats.submissions);
1105 assert_eq!(engine.nowait_supported, !unsupported);
1106 }
1107 }
1108 }
1109
1110 #[test]
1111 fn warm_cohorts_reprobe_and_keep_original_indices_when_the_next_cohort_is_cold() {
1112 let root = tempfile::tempdir().unwrap();
1113 let path = root.path().join("file");
1114 let expected: Vec<u8> = (0..128).collect();
1115 std::fs::write(&path, &expected).unwrap();
1116 let file = Arc::new(File::open(path).unwrap());
1117 let (mut engine, telemetry) =
1118 Engine::new(Options::default().with_cached_reads(true)).unwrap();
1119 engine.cache_read_limit = Some(usize::MAX);
1120 engine.cache_read_error = Some((2, libc::EAGAIN));
1121 let mut buffers = [[99; 4]; 32];
1122 let mut operations: Vec<_> = buffers
1123 .iter_mut()
1124 .enumerate()
1125 .map(|(i, buffer)| Read {
1126 file: &file,
1127 offset: (i * 4) as u64,
1128 buffer,
1129 })
1130 .collect();
1131 let results = engine.read(&mut operations);
1132 assert_eq!(results.len(), 32);
1133 assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1134 assert_eq!(buffers.concat(), expected);
1135 let stats = telemetry.snapshot();
1136 assert_eq!(
1137 (stats.cache_probes, stats.cache_hits, stats.syscall_reads),
1138 (3, 2, 14)
1139 );
1140 assert_eq!(
1141 (
1142 stats.cache_misses,
1143 stats.waves,
1144 stats.submissions,
1145 stats.completions
1146 ),
1147 (16, 1, 16, 16)
1148 );
1149 }
1150
1151 #[test]
1152 fn cached_reads_keep_eof_tail_and_stop_after_a_real_error() {
1153 let root = tempfile::tempdir().unwrap();
1154 let path = root.path().join("file");
1155 std::fs::write(&path, b"abc").unwrap();
1156 let file = Arc::new(File::open(&path).unwrap());
1157 let writeonly = Arc::new(File::options().write(true).open(path).unwrap());
1158 let (mut engine, telemetry) =
1159 Engine::new(Options::default().with_cached_reads(true)).unwrap();
1160 engine.cache_read_limit = Some(usize::MAX);
1161 let mut bytes = [99; 5];
1162 let results = engine.read(&mut [Read {
1163 file: &file,
1164 offset: 0,
1165 buffer: &mut bytes,
1166 }]);
1167 assert_eq!(*results[0].as_ref().unwrap(), 3);
1168 assert_eq!(bytes, [b'a', b'b', b'c', 99, 99]);
1169 assert_eq!(
1170 telemetry.snapshot().submissions,
1171 1,
1172 "partial cache hits must probe EOF"
1173 );
1174 let before = telemetry.snapshot().cache_probes;
1175 assert_eq!(
1176 *engine.read(&mut [Read {
1177 file: &writeonly,
1178 offset: 0,
1179 buffer: &mut [],
1180 }])[0]
1181 .as_ref()
1182 .unwrap(),
1183 0
1184 );
1185 assert_eq!(
1186 telemetry.snapshot().cache_probes,
1187 before,
1188 "empty reads do not issue syscalls"
1189 );
1190 engine.cache_read_limit = Some(0);
1191 let results = engine.read(&mut [Read {
1192 file: &file,
1193 offset: 0,
1194 buffer: &mut bytes[..3],
1195 }]);
1196 assert_eq!(
1197 *results[0].as_ref().unwrap(),
1198 3,
1199 "a zero NOWAIT reply cannot establish EOF"
1200 );
1201 assert_eq!(&bytes[..3], b"abc");
1202 engine.cache_read_limit = Some(usize::MAX);
1203 let mut first = [99; 3];
1204 let mut invalid = [99; 3];
1205 let mut untouched = [99; 3];
1206 let results = engine.read(&mut [
1207 Read {
1208 file: &file,
1209 offset: 0,
1210 buffer: &mut first,
1211 },
1212 Read {
1213 file: &writeonly,
1214 offset: 0,
1215 buffer: &mut invalid,
1216 },
1217 Read {
1218 file: &file,
1219 offset: 0,
1220 buffer: &mut untouched,
1221 },
1222 ]);
1223 assert_eq!(*results[0].as_ref().unwrap(), 3);
1224 assert_eq!(
1225 results[1].as_ref().unwrap_err().raw_os_error(),
1226 Some(libc::EBADF)
1227 );
1228 assert_eq!(*results[2].as_ref().unwrap(), 0);
1229 assert_eq!(first, *b"abc");
1230 assert_eq!((invalid, untouched), ([99; 3], [99; 3]));
1231 assert_eq!(telemetry.snapshot().submissions, 2);
1232 engine.ring.take();
1233 assert!(
1234 engine.read(&mut [Read {
1235 file: &file,
1236 offset: 0,
1237 buffer: &mut untouched
1238 }])[0]
1239 .is_err()
1240 );
1241 assert_eq!(
1242 untouched, [99; 3],
1243 "poisoned executors must not bypass the ring through cache hits"
1244 );
1245 }
1246
1247 #[test]
1248 fn cooperative_ring_can_move_between_threads_after_draining_each_call() {
1249 let root = tempfile::tempdir().unwrap();
1250 let path = root.path().join("file");
1251 std::fs::write(&path, b"old").unwrap();
1252 let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1253 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1254 let (mut engine, file) = std::thread::spawn(move || {
1255 assert_eq!(
1256 *engine.write(&[Write {
1257 file: &file,
1258 offset: 0,
1259 data: b"new"
1260 }])[0]
1261 .as_ref()
1262 .unwrap(),
1263 3
1264 );
1265 (engine, file)
1266 })
1267 .join()
1268 .unwrap();
1269 let mut bytes = [0; 3];
1270 assert_eq!(
1271 *engine.read(&mut [Read {
1272 file: &file,
1273 offset: 0,
1274 buffer: &mut bytes
1275 }])[0]
1276 .as_ref()
1277 .unwrap(),
1278 3
1279 );
1280 assert_eq!(bytes, *b"new");
1281 let stats = telemetry.snapshot();
1282 assert_eq!(
1283 (stats.waves, stats.submissions, stats.completions),
1284 (2, 2, 2)
1285 );
1286 }
1287
1288 #[test]
1289 fn merged_short_writes_preserve_each_requests_contiguous_progress() {
1290 for short in [1, 3, 4, 6] {
1291 let root = tempfile::tempdir().unwrap();
1292 let path = root.path().join("file");
1293 std::fs::write(&path, b"00000000").unwrap();
1294 let file = Arc::new(File::options().write(true).open(&path).unwrap());
1295 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1296 engine.short_completion = Some(short);
1297 let results = engine.write(&[
1298 Write {
1299 file: &file,
1300 offset: 0,
1301 data: b"abc",
1302 },
1303 Write {
1304 file: &file,
1305 offset: 3,
1306 data: b"defgh",
1307 },
1308 ]);
1309 assert_eq!(*results[0].as_ref().unwrap(), 3);
1310 assert_eq!(*results[1].as_ref().unwrap(), 5);
1311 assert_eq!(std::fs::read(&path).unwrap(), b"abcdefgh");
1312 let waves = 8usize.div_ceil(short) as u64;
1313 let stats = telemetry.snapshot();
1314 assert_eq!(
1315 (stats.waves, stats.submissions, stats.completions),
1316 (waves, waves, waves)
1317 );
1318 }
1319 }
1320
1321 #[test]
1322 fn merged_short_write_is_not_replayed_when_another_completion_fails() {
1323 for short in [1, 3, 4, 6] {
1324 let root = tempfile::tempdir().unwrap();
1325 let path = root.path().join("file");
1326 std::fs::write(&path, b"00000000").unwrap();
1327 let file = Arc::new(File::options().write(true).open(&path).unwrap());
1328 let readonly = Arc::new(File::open(&path).unwrap());
1329 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1330 engine.short_completion = Some(short);
1331 let results = engine.write(&[
1332 Write {
1333 file: &file,
1334 offset: 0,
1335 data: b"abc",
1336 },
1337 Write {
1338 file: &file,
1339 offset: 3,
1340 data: b"defgh",
1341 },
1342 Write {
1343 file: &readonly,
1344 offset: 0,
1345 data: b"bad",
1346 },
1347 ]);
1348 assert_eq!(*results[0].as_ref().unwrap(), short.min(3));
1349 assert_eq!(*results[1].as_ref().unwrap(), short.saturating_sub(3));
1350 assert_eq!(
1351 results[2].as_ref().unwrap_err().raw_os_error(),
1352 Some(libc::EBADF)
1353 );
1354 let mut expected = *b"00000000";
1355 expected[..short].copy_from_slice(&b"abcdefgh"[..short]);
1356 assert_eq!(std::fs::read(&path).unwrap(), expected);
1357 let stats = telemetry.snapshot();
1358 assert_eq!(
1359 (stats.waves, stats.submissions, stats.completions),
1360 (1, 2, 2)
1361 );
1362 }
1363 }
1364
1365 #[test]
1366 fn merged_zero_progress_and_failed_writes_never_retry_the_group() {
1367 for zero_progress in [false, true] {
1368 let root = tempfile::tempdir().unwrap();
1369 let path = root.path().join("file");
1370 std::fs::write(&path, b"000000").unwrap();
1371 let file = Arc::new(
1372 File::options()
1373 .read(true)
1374 .write(zero_progress)
1375 .open(&path)
1376 .unwrap(),
1377 );
1378 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1379 engine.short_completion = zero_progress.then_some(0);
1380 let results = engine.write(&[
1381 Write {
1382 file: &file,
1383 offset: 0,
1384 data: b"abc",
1385 },
1386 Write {
1387 file: &file,
1388 offset: 3,
1389 data: b"DEF",
1390 },
1391 ]);
1392 let errno = if zero_progress {
1393 libc::EIO
1394 } else {
1395 libc::EBADF
1396 };
1397 assert!(
1398 results
1399 .iter()
1400 .all(|result| result.as_ref().unwrap_err().raw_os_error() == Some(errno))
1401 );
1402 assert_eq!(std::fs::read(path).unwrap(), b"000000");
1403 let stats = telemetry.snapshot();
1404 assert_eq!(
1405 (stats.waves, stats.submissions, stats.completions),
1406 (1, 1, 1)
1407 );
1408 }
1409 }
1410
1411 #[test]
1412 fn waves_reuse_bounded_storage_and_wait_once_for_all_completions() {
1413 let root = tempfile::tempdir().unwrap();
1414 let path = root.path().join("file");
1415 std::fs::write(&path, b"0123456789").unwrap();
1416 let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1417 let (mut engine, telemetry) = Engine::new(
1418 Options::default().with_max_batch_bytes(std::num::NonZeroUsize::new(7).unwrap()),
1419 )
1420 .unwrap();
1421 for value in *b"abc" {
1422 let bytes = [value; 10];
1423 let results = engine.write(&[Write {
1424 file: &file,
1425 offset: 0,
1426 data: &bytes,
1427 }]);
1428 assert_eq!(*results[0].as_ref().unwrap(), 10);
1429 let mut actual = [0; 10];
1430 let results = engine.read(&mut [Read {
1431 file: &file,
1432 offset: 0,
1433 buffer: &mut actual,
1434 }]);
1435 assert_eq!(*results[0].as_ref().unwrap(), 10);
1436 assert_eq!(actual, bytes);
1437 assert!(engine.scratch.capacity() <= 7);
1438 }
1439 let mut tail = [99; 7];
1441 let results = engine.read(&mut [Read {
1442 file: &file,
1443 offset: 8,
1444 buffer: &mut tail,
1445 }]);
1446 assert_eq!(*results[0].as_ref().unwrap(), 2);
1447 assert_eq!(tail, [b'c', b'c', 99, 99, 99, 99, 99]);
1448 let stats = telemetry.snapshot();
1449 assert_eq!(stats.scratch_growths, 1);
1450 assert_eq!((stats.peak_bytes, stats.waves), (7, 13));
1451 assert_eq!(
1452 (stats.enters, stats.submissions, stats.completions),
1453 (13, 13, 13)
1454 );
1455 }
1456
1457 #[test]
1458 fn many_small_requests_share_one_arena_and_one_enter() {
1459 let root = tempfile::tempdir().unwrap();
1460 let path = root.path().join("file");
1461 std::fs::write(&path, vec![42; 1024]).unwrap();
1462 let file = Arc::new(File::open(&path).unwrap());
1463 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1464 for _ in 0..3 {
1465 let mut bytes = vec![[0; 4]; 256];
1466 let mut operations: Vec<_> = bytes
1467 .iter_mut()
1468 .enumerate()
1469 .map(|(i, buffer)| Read {
1470 file: &file,
1471 offset: (i * 4) as u64,
1472 buffer,
1473 })
1474 .collect();
1475 let results = engine.read(&mut operations);
1476 assert!(results.iter().all(|result| *result.as_ref().unwrap() == 4));
1477 assert!(bytes.iter().all(|buffer| buffer == &[42; 4]));
1478 }
1479 let stats = telemetry.snapshot();
1480 assert_eq!(
1481 (stats.waves, stats.enters, stats.scratch_growths),
1482 (3, 3, 1)
1483 );
1484 assert_eq!((stats.submissions, stats.completions), (768, 768));
1485 let results = engine.read_owned(
1486 (0..256)
1487 .map(|i| OwnedRead {
1488 file: Arc::clone(&file),
1489 offset: (i * 4) as u64,
1490 buffer: vec![0; 4],
1491 })
1492 .collect(),
1493 );
1494 assert!(
1495 results
1496 .iter()
1497 .all(|result| result.as_ref().unwrap() == &[42; 4])
1498 );
1499 let stats = telemetry.snapshot();
1500 assert_eq!(
1501 (stats.waves, stats.enters, stats.scratch_growths),
1502 (4, 4, 1)
1503 );
1504 assert_eq!((stats.submissions, stats.completions), (1024, 1024));
1505 assert_eq!(
1506 stats.copied_read_bytes,
1507 3 * 1024,
1508 "the owned cohort adds no copies"
1509 );
1510 }
1511
1512 #[test]
1513 fn eof_relative_reads_observe_external_growth_and_truncation() {
1514 let root = tempfile::tempdir().unwrap();
1515 let path = root.path().join("file");
1516 std::fs::write(&path, b"abc").unwrap();
1517 let (engine, _) = Engine::new(Options::default()).unwrap();
1518 let mut fs = vfsi_local::LocalBackend::new(root.path().to_path_buf(), engine).unwrap();
1519 let file = fs
1520 .open_raw_impl(std::path::Path::new("/file"), libc::O_RDONLY, 0)
1521 .unwrap();
1522 for bytes in [b"abcdefgh".as_slice(), b"x"] {
1523 std::fs::write(&path, bytes).unwrap();
1524 let results = fs
1525 .vread_impl(&[
1526 ReadOp {
1527 file: file.clone(),
1528 offset: VfOffset::At(0),
1529 length: 10,
1530 },
1531 ReadOp {
1532 file: file.clone(),
1533 offset: VfOffset::End,
1534 length: 1,
1535 },
1536 ])
1537 .unwrap();
1538 assert_eq!(results[0].data, bytes);
1539 assert!(results[0].eof);
1540 assert_eq!(results[1].offset, bytes.len() as u64);
1541 assert!(results[1].eof);
1542 assert!(results[1].data.is_empty());
1543 assert_eq!(
1544 fs.seek_raw_impl(&file, 0, vfsi_core::SeekFrom::Cur)
1545 .unwrap(),
1546 0
1547 );
1548 }
1549 fs.close_impl(&file).unwrap();
1550 }
1551
1552 #[test]
1553 fn injected_short_writes_complete_contiguous_suffixes_and_short_reads_stop() {
1554 let root = tempfile::tempdir().unwrap();
1555 let path = root.path().join("file");
1556 std::fs::write(&path, b"0000000000").unwrap();
1557 let file = Arc::new(File::options().read(true).write(true).open(&path).unwrap());
1558 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1559 engine.short_completion = Some(2);
1560 assert_eq!(
1561 engine.write(&[Write {
1562 file: &file,
1563 offset: 0,
1564 data: b"abcdefghij"
1565 }])[0]
1566 .as_ref()
1567 .unwrap(),
1568 &10
1569 );
1570 assert_eq!(std::fs::read(&path).unwrap(), b"abcdefghij");
1571 assert_eq!(telemetry.snapshot().waves, 5);
1572 let mut bytes = [99; 10];
1573 assert_eq!(
1574 engine.read(&mut [Read {
1575 file: &file,
1576 offset: 0,
1577 buffer: &mut bytes
1578 }])[0]
1579 .as_ref()
1580 .unwrap(),
1581 &2
1582 );
1583 assert_eq!(bytes, [b'a', b'b', 99, 99, 99, 99, 99, 99, 99, 99]);
1584 assert_eq!(
1585 telemetry.snapshot().submissions,
1586 telemetry.snapshot().completions
1587 );
1588 }
1589
1590 #[test]
1591 fn duplicate_cursor_reads_and_append_remain_ordered() {
1592 let root = tempfile::tempdir().unwrap();
1593 std::fs::write(root.path().join("file"), b"abcdef").unwrap();
1594 let (engine, telemetry) = Engine::new(Options::default()).unwrap();
1595 let mut fs = vfsi_local::LocalBackend::new(root.path().to_path_buf(), engine).unwrap();
1596 let file = fs
1597 .open_raw_impl(std::path::Path::new("/file"), libc::O_RDWR, 0)
1598 .unwrap();
1599 let read = ReadOp {
1600 file: file.clone(),
1601 offset: VfOffset::Cur,
1602 length: 2,
1603 };
1604 let results = fs.vread_impl(&[read.clone(), read]).unwrap();
1605 assert_eq!(results[0].data, b"ab");
1606 assert_eq!(results[1].data, b"cd");
1607 assert_eq!(telemetry.snapshot().submissions, 0);
1608 let positional = ReadOp {
1609 file: file.clone(),
1610 offset: VfOffset::At(0),
1611 length: 2,
1612 };
1613 fs.vread_impl(&[positional]).unwrap();
1614 assert_eq!(
1615 fs.seek_raw_impl(&file, 0, vfsi_core::SeekFrom::Cur)
1616 .unwrap(),
1617 4
1618 );
1619 let append = fs
1620 .open_raw_impl(
1621 std::path::Path::new("/file"),
1622 libc::O_WRONLY | libc::O_APPEND,
1623 0,
1624 )
1625 .unwrap();
1626 fs.vwrite_impl(&[WriteOp::new(&append, VfOffset::At(0), b"xy".as_slice())])
1627 .unwrap();
1628 assert_eq!(
1629 std::fs::read(root.path().join("file")).unwrap(),
1630 b"abcdefxy"
1631 );
1632 fs.close_impl(&file).unwrap();
1633 fs.close_impl(&append).unwrap();
1634 }
1635
1636 #[test]
1637 fn failure_does_not_finish_another_jobs_short_write() {
1638 let root = tempfile::tempdir().unwrap();
1639 let path = root.path().join("file");
1640 std::fs::write(&path, b"000").unwrap();
1641 let writable = Arc::new(File::options().write(true).open(&path).unwrap());
1642 let readonly = Arc::new(File::open(&path).unwrap());
1643 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1644 engine.short_completion = Some(2);
1645 let results = engine.write(&[
1646 Write {
1647 file: &writable,
1648 offset: 0,
1649 data: b"abc",
1650 },
1651 Write {
1652 file: &readonly,
1653 offset: 0,
1654 data: b"xyz",
1655 },
1656 ]);
1657 assert_eq!(*results[0].as_ref().unwrap(), 2);
1658 assert_eq!(
1659 results[1].as_ref().unwrap_err().raw_os_error(),
1660 Some(libc::EBADF)
1661 );
1662 assert_eq!(std::fs::read(path).unwrap(), b"ab0");
1663 let stats = telemetry.snapshot();
1664 assert_eq!(
1665 (stats.waves, stats.submissions, stats.completions),
1666 (1, 2, 2)
1667 );
1668 }
1669
1670 #[test]
1671 fn failure_after_publication_retains_inflight_ownership_without_replay() {
1672 let root = tempfile::tempdir().unwrap();
1673 let path = root.path().join("file");
1674 std::fs::write(&path, b"old").unwrap();
1675 let file = Arc::new(File::options().write(true).open(&path).unwrap());
1676 let weak = Arc::downgrade(&file);
1677 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1678 engine.fail_after_submit = true;
1679 let results = engine.write(&[
1680 Write {
1681 file: &file,
1682 offset: 0,
1683 data: b"ne",
1684 },
1685 Write {
1686 file: &file,
1687 offset: 2,
1688 data: b"w",
1689 },
1690 ]);
1691 assert!(results.iter().all(Result::is_err));
1692 assert!(engine.ring.is_none());
1693 assert_eq!(telemetry.snapshot().submissions, 1);
1694 assert_eq!(telemetry.snapshot().completions, 0);
1695 assert!(
1696 engine.scratch.is_empty(),
1697 "in-flight arena is retained, not reused"
1698 );
1699 assert!(
1700 engine.write(&[Write {
1701 file: &file,
1702 offset: 0,
1703 data: b"bad"
1704 }])[0]
1705 .is_err()
1706 );
1707 assert_eq!(telemetry.snapshot().submissions, 1);
1708 assert_eq!(std::fs::read(&path).unwrap(), b"new");
1709 assert!(
1710 engine.scratch.is_empty(),
1711 "poisoned calls must not grow a new arena"
1712 );
1713 drop(engine);
1714 drop(file);
1715 assert!(weak.upgrade().is_some());
1718 }
1719
1720 #[test]
1721 fn poisoned_executor_does_not_submit_or_replay_writes() {
1722 let root = tempfile::tempdir().unwrap();
1723 let path = root.path().join("file");
1724 std::fs::write(&path, b"old").unwrap();
1725 let file = Arc::new(File::options().write(true).open(&path).unwrap());
1726 let (mut engine, telemetry) = Engine::new(Options::default()).unwrap();
1727 engine.ring.take();
1728 assert!(
1729 engine.write(&[Write {
1730 file: &file,
1731 offset: 0,
1732 data: b"new"
1733 }])[0]
1734 .is_err()
1735 );
1736 assert_eq!(std::fs::read(&path).unwrap(), b"old");
1737 assert_eq!(telemetry.snapshot().submissions, 0);
1738 }
1739}