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