1use bytes::Bytes;
4#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
5use std::cell::RefCell;
6#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
7use std::collections::HashMap;
8use std::collections::VecDeque;
9use std::fs::File;
10use std::future::Future;
11use std::io::{self, Read};
12use std::path::Path;
13use std::pin::Pin;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak, mpsc};
16use std::task::{Context, Poll, Waker};
17
18type FileSequencer = Arc<Mutex<Option<Arc<OperationFence>>>>;
19
20#[cfg(windows)]
21type FileIdentity = windows::FileIdentity;
22#[cfg(target_os = "linux")]
23type FileIdentity = linux::FileIdentity;
24#[cfg(target_vendor = "apple")]
25type FileIdentity = apple::FileIdentity;
26
27#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
28struct FileHealth {
29 tail: FileSequencer,
30 uncertain: Arc<AtomicBool>,
31 poison_anchor: Option<Arc<File>>,
35}
36
37#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
38static FILE_HEALTH: OnceLock<Mutex<FileHealthRegistry>> = OnceLock::new();
39
40#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
41#[derive(Default)]
42struct FileHealthRegistry {
43 entries: HashMap<FileIdentity, FileHealth>,
44}
45
46#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
47impl FileHealthRegistry {
48 fn sweep_idle(&mut self) {
49 self.entries.retain(|_, health| {
50 health.poison_anchor.is_some()
51 || Arc::strong_count(&health.tail) > 1
52 || Arc::strong_count(&health.uncertain) > 1
53 });
54 }
55}
56
57#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
58fn file_identity(file: &File) -> io::Result<FileIdentity> {
59 #[cfg(windows)]
60 return windows::file_identity(file);
61 #[cfg(target_os = "linux")]
62 return linux::file_identity(file);
63 #[cfg(target_vendor = "apple")]
64 apple::file_identity(file)
65}
66
67#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
68thread_local! {
69 static ACTIVE_FILES: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
70}
71
72#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
73struct FileScope(usize);
74
75#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
76impl FileScope {
77 fn enter(tail: &FileSequencer) -> Self {
78 let identity = Arc::as_ptr(tail) as usize;
79 ACTIVE_FILES.with_borrow_mut(|active| active.push(identity));
80 Self(identity)
81 }
82
83 fn contains(tail: &FileSequencer) -> bool {
84 let identity = Arc::as_ptr(tail) as usize;
85 ACTIVE_FILES.with_borrow(|active| active.contains(&identity))
86 }
87}
88
89#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
90impl Drop for FileScope {
91 fn drop(&mut self) {
92 ACTIVE_FILES.with_borrow_mut(|active| {
93 assert_eq!(
94 active.pop(),
95 Some(self.0),
96 "native file scope order changed"
97 );
98 });
99 }
100}
101
102#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
103fn shared_file_health(file: &File) -> io::Result<(FileSequencer, Arc<AtomicBool>)> {
104 let identity = file_identity(file)?;
105 let registry_lock = FILE_HEALTH.get_or_init(|| Mutex::new(FileHealthRegistry::default()));
106 let mut registry = registry_lock
107 .lock()
108 .unwrap_or_else(std::sync::PoisonError::into_inner);
109 if let Some(health) = registry.entries.get(&identity) {
110 if health.poison_anchor.is_some()
111 || Arc::strong_count(&health.tail) > 1
112 || Arc::strong_count(&health.uncertain) > 1
113 {
114 return Ok((Arc::clone(&health.tail), Arc::clone(&health.uncertain)));
115 }
116 registry.entries.remove(&identity);
119 }
120 #[cfg(target_os = "linux")]
121 let capacity = linux::file_identity_capacity();
122 #[cfg(target_vendor = "apple")]
123 let capacity = apple::file_identity_capacity();
124 #[cfg(windows)]
125 let capacity: usize = 4096;
126 if registry.entries.len() >= capacity {
129 registry.sweep_idle();
130 }
131 if registry.entries.len() >= capacity {
132 return Err(io::Error::other("native file admission capacity exhausted"));
133 }
134 let tail = Arc::new(Mutex::new(None));
135 let uncertain = Arc::new(AtomicBool::new(false));
136 registry.entries.insert(
137 identity,
138 FileHealth {
139 tail: Arc::clone(&tail),
140 uncertain: Arc::clone(&uncertain),
141 poison_anchor: None,
142 },
143 );
144 Ok((tail, uncertain))
145}
146
147#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
148fn poison_file_health(file: &Arc<File>, uncertain: &Arc<AtomicBool>) {
149 let identity = file_identity(file).unwrap_or_else(|_| std::process::abort());
150 let registry_lock = FILE_HEALTH.get().unwrap_or_else(|| std::process::abort());
151 let mut registry = registry_lock
152 .lock()
153 .unwrap_or_else(std::sync::PoisonError::into_inner);
154 let health = registry
155 .entries
156 .get_mut(&identity)
157 .unwrap_or_else(|| std::process::abort());
158 if !Arc::ptr_eq(&health.uncertain, uncertain) {
159 std::process::abort();
160 }
161 health.poison_anchor.get_or_insert_with(|| Arc::clone(file));
162 uncertain.store(true, Ordering::Release);
163}
164
165#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
166fn poison_borrowed_file_health(file: &File, uncertain: &Arc<AtomicBool>) {
167 let anchor = Arc::new(file.try_clone().unwrap_or_else(|_| std::process::abort()));
168 poison_file_health(&anchor, uncertain);
169}
170
171#[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
172fn with_file_admission<T>(file: &File, operation: impl FnOnce() -> io::Result<T>) -> io::Result<T> {
173 let (tail, uncertain) = shared_file_health(file)?;
174 if FileScope::contains(&tail) {
178 if uncertain.load(Ordering::Acquire) {
179 return Err(io::Error::other(
180 "prior native I/O completion on this file is uncertain",
181 ));
182 }
183 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation))
184 .unwrap_or_else(|_| std::process::abort());
185 if result.as_ref().err().is_some_and(is_uncertain_io_error) {
186 poison_borrowed_file_health(file, &uncertain);
187 }
188 return result;
189 }
190 let (predecessor, completion) = {
191 let mut last = tail
192 .lock()
193 .unwrap_or_else(std::sync::PoisonError::into_inner);
194 let predecessor = last.clone();
195 let completion = Arc::new(OperationFence::default());
196 *last = Some(Arc::clone(&completion));
197 (predecessor, completion)
198 };
199 if let Some(predecessor) = predecessor {
200 predecessor.wait_until_complete();
201 }
202 if uncertain.load(Ordering::Acquire) {
203 completion.complete();
204 return Err(io::Error::other(
205 "prior native I/O completion on this file is uncertain",
206 ));
207 }
208 let scope = FileScope::enter(&tail);
209 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation))
210 .unwrap_or_else(|_| std::process::abort());
211 if result.as_ref().err().is_some_and(is_uncertain_io_error) {
212 poison_borrowed_file_health(file, &uncertain);
213 }
214 drop(scope);
215 completion.complete();
216 result
217}
218
219pub struct NativeFile {
228 file: Arc<File>,
229 tail: FileSequencer,
230 uncertain: Arc<AtomicBool>,
231 #[cfg(windows)]
232 overlapped: bool,
233}
234
235impl NativeFile {
236 pub fn from_file(file: File) -> io::Result<Self> {
238 let file = Arc::new(file);
239 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
240 let (tail, uncertain) = shared_file_health(&file)?;
241 #[cfg(not(any(windows, target_os = "linux", target_vendor = "apple")))]
242 let (tail, uncertain) = (Arc::new(Mutex::new(None)), Arc::new(AtomicBool::new(false)));
243 Ok(Self {
244 file,
245 tail,
246 uncertain,
247 #[cfg(windows)]
248 overlapped: false,
249 })
250 }
251
252 #[cfg(windows)]
260 #[allow(unsafe_code)]
261 pub unsafe fn from_overlapped_file_unchecked(file: File) -> io::Result<Self> {
262 let mut native = Self::from_file(file)?;
263 native.overlapped = true;
264 Ok(native)
265 }
266
267 #[must_use]
269 pub fn set_len_async(&self, size: u64) -> UnitCompletion {
270 UnitCompletion::submit(
271 Arc::clone(&self.file),
272 NativeUnitOperation::SetLen(size),
273 Some(Arc::clone(&self.tail)),
274 Some(Arc::clone(&self.uncertain)),
275 #[cfg(windows)]
276 self.overlapped,
277 )
278 }
279
280 #[must_use]
284 pub fn control_async(
285 &self,
286 operation: impl FnOnce(&File) -> io::Result<()> + Send + 'static,
287 ) -> UnitCompletion {
288 UnitCompletion::submit(
289 Arc::clone(&self.file),
290 NativeUnitOperation::Control(Box::new(operation)),
291 Some(Arc::clone(&self.tail)),
292 Some(Arc::clone(&self.uncertain)),
293 #[cfg(windows)]
294 self.overlapped,
295 )
296 }
297
298 #[must_use]
300 pub fn read_batch_async(&self, reads: Vec<OwnedRead>) -> ReadBatch {
301 ReadBatch::submit(
302 Arc::clone(&self.file),
303 reads,
304 Some(Arc::clone(&self.tail)),
305 Some(Arc::clone(&self.uncertain)),
306 #[cfg(windows)]
307 self.overlapped,
308 )
309 }
310
311 #[must_use]
313 pub fn write_all_batch_async(&self, writes: Vec<OwnedWrite>) -> UnitCompletion {
314 UnitCompletion::submit(
315 Arc::clone(&self.file),
316 NativeUnitOperation::Write(writes),
317 Some(Arc::clone(&self.tail)),
318 Some(Arc::clone(&self.uncertain)),
319 #[cfg(windows)]
320 self.overlapped,
321 )
322 }
323
324 #[must_use]
326 pub fn sync_async(&self, durability: Durability) -> UnitCompletion {
327 UnitCompletion::submit(
328 Arc::clone(&self.file),
329 NativeUnitOperation::Sync(durability),
330 Some(Arc::clone(&self.tail)),
331 Some(Arc::clone(&self.uncertain)),
332 #[cfg(windows)]
333 self.overlapped,
334 )
335 }
336}
337
338#[cfg(target_vendor = "apple")]
339mod apple;
340#[cfg(target_os = "linux")]
341mod linux;
342mod process_tree;
343#[cfg(windows)]
344mod windows;
345
346pub use process_tree::ProcessTree;
347
348pub fn is_uncertain_io_error(error: &io::Error) -> bool {
353 #[cfg(target_os = "linux")]
354 {
355 linux::is_uncertain_completion(error)
356 }
357 #[cfg(windows)]
358 {
359 let _ = error;
360 false
361 }
362 #[cfg(not(any(target_os = "linux", windows)))]
363 {
364 let _ = error;
365 false
366 }
367}
368
369#[derive(Clone)]
375pub struct OwnershipAnchor {
376 _owner: Arc<dyn Send + Sync>,
379 release: OwnershipReleaseNotifier,
380}
381
382#[derive(Clone)]
383struct OwnershipReleaseNotifier {
384 owner: Weak<dyn Send + Sync>,
385 released: Arc<(Mutex<()>, Condvar)>,
386}
387
388impl Drop for OwnershipReleaseNotifier {
389 fn drop(&mut self) {
390 if self.owner.strong_count() == 0 {
391 let (lock, released) = &*self.released;
392 let _guard = lock
393 .lock()
394 .unwrap_or_else(std::sync::PoisonError::into_inner);
395 released.notify_all();
396 }
397 }
398}
399
400impl OwnershipAnchor {
401 pub fn new(owner: impl Send + Sync + 'static) -> Self {
403 let owner: Arc<dyn Send + Sync> = Arc::new(owner);
404 Self {
405 release: OwnershipReleaseNotifier {
406 owner: Arc::downgrade(&owner),
407 released: Arc::new((Mutex::new(()), Condvar::new())),
408 },
409 _owner: owner,
410 }
411 }
412
413 #[must_use]
415 pub fn release_barrier(&self) -> OwnershipReleaseBarrier {
416 OwnershipReleaseBarrier {
417 owner: self.release.owner.clone(),
418 released: Arc::clone(&self.release.released),
419 }
420 }
421}
422
423pub struct OwnershipReleaseBarrier {
428 owner: Weak<dyn Send + Sync>,
429 released: Arc<(Mutex<()>, Condvar)>,
430}
431
432impl OwnershipReleaseBarrier {
433 #[must_use]
435 pub fn is_released(&self) -> bool {
436 self.owner.strong_count() == 0
437 }
438
439 pub fn wait(self) {
441 let (lock, released) = &*self.released;
442 let mut guard = lock
443 .lock()
444 .unwrap_or_else(std::sync::PoisonError::into_inner);
445 while self.owner.strong_count() != 0 {
446 guard = released
447 .wait(guard)
448 .unwrap_or_else(std::sync::PoisonError::into_inner);
449 }
450 }
451}
452
453#[derive(Clone, Copy, Debug, Eq, PartialEq)]
455pub enum Durability {
456 Full,
458 Barrier,
463}
464
465pub fn sync_file(file: &File, durability: Durability) -> io::Result<()> {
467 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
468 return with_file_admission(file, || sync_file_unsequenced(file, durability));
469 #[cfg(not(any(windows, target_os = "linux", target_vendor = "apple")))]
470 sync_file_unsequenced(file, durability)
471}
472
473fn sync_file_unsequenced(file: &File, durability: Durability) -> io::Result<()> {
474 match durability {
475 Durability::Full => file.sync_all(),
476 Durability::Barrier => barrier_sync(file),
477 }
478}
479
480pub fn sync_data(file: &File, durability: Durability) -> io::Result<()> {
482 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
483 return with_file_admission(file, || sync_data_unsequenced(file, durability));
484 #[cfg(not(any(windows, target_os = "linux", target_vendor = "apple")))]
485 sync_data_unsequenced(file, durability)
486}
487
488fn sync_data_unsequenced(file: &File, durability: Durability) -> io::Result<()> {
489 match durability {
490 Durability::Full => file.sync_data(),
491 Durability::Barrier => barrier_sync(file),
492 }
493}
494
495pub fn sync_parent(path: &Path, durability: Durability) -> io::Result<()> {
497 sync_parent_impl(path, durability)
498}
499
500#[must_use]
503pub fn is_exclusive_lock_contention(error: &io::Error) -> bool {
504 error.kind() == io::ErrorKind::WouldBlock || cfg!(windows) && error.raw_os_error() == Some(33)
505}
506
507#[derive(Clone, Copy, Debug, Eq, PartialEq)]
509pub enum RenameMode {
510 NoReplace,
512 Replace,
514}
515
516pub fn durable_rename(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
518 durable_rename_impl(from, to, mode)
519}
520
521pub fn read_at(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
523 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
524 return with_file_admission(file, || read_at_impl(file, offset, destination));
525 #[cfg(not(any(windows, target_os = "linux", target_vendor = "apple")))]
526 read_at_impl(file, offset, destination)
527}
528
529pub fn write_all_at(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
531 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
532 return with_file_admission(file, || write_all_at_impl(file, offset, bytes));
533 #[cfg(not(any(windows, target_os = "linux", target_vendor = "apple")))]
534 write_all_at_impl(file, offset, bytes)
535}
536
537#[cfg(windows)]
539pub fn set_file_attributes(path: &Path, attributes: u32) -> io::Result<()> {
540 windows::set_file_attributes(path, attributes)
541}
542
543#[derive(Clone, Debug, Eq, PartialEq)]
545pub struct OwnedWrite {
546 pub offset: u64,
548 pub bytes: Bytes,
550}
551
552#[derive(Clone, Copy, Debug, Eq, PartialEq)]
554pub struct OwnedRead {
555 pub offset: u64,
557 pub length: usize,
559}
560
561pub fn read_batch_async(file: File, reads: Vec<OwnedRead>) -> ReadBatch {
567 let file = Arc::new(file);
568 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
569 {
570 match shared_file_health(&file) {
571 Ok((tail, uncertain)) => ReadBatch::submit(
572 file,
573 reads,
574 Some(tail),
575 Some(uncertain),
576 #[cfg(windows)]
577 false,
578 ),
579 Err(error) => ReadBatch::failed(error),
580 }
581 }
582 #[cfg(not(any(windows, target_os = "linux", target_vendor = "apple")))]
583 ReadBatch::submit(file, reads, None, None)
584}
585
586pub fn write_all_batch_async(file: File, writes: Vec<OwnedWrite>) -> UnitCompletion {
591 let file = Arc::new(file);
592 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
593 {
594 match shared_file_health(&file) {
595 Ok((tail, uncertain)) => UnitCompletion::submit(
596 file,
597 NativeUnitOperation::Write(writes),
598 Some(tail),
599 Some(uncertain),
600 #[cfg(windows)]
601 false,
602 ),
603 Err(error) => UnitCompletion::failed(error),
604 }
605 }
606 #[cfg(not(any(windows, target_os = "linux", target_vendor = "apple")))]
607 UnitCompletion::submit(file, NativeUnitOperation::Write(writes), None, None)
608}
609
610pub fn spawn_service_process(executable: &Path) -> io::Result<()> {
617 #[cfg(windows)]
618 {
619 windows::spawn_service_process(executable)
620 }
621 #[cfg(unix)]
622 {
623 use std::os::unix::process::CommandExt as _;
624
625 let mut command = std::process::Command::new(executable);
626 command
627 .arg("__service")
628 .stdin(std::process::Stdio::null())
629 .stdout(std::process::Stdio::null())
630 .stderr(std::process::Stdio::null());
631 command.process_group(0);
634 command.spawn().map(drop)
635 }
636 #[cfg(not(any(windows, unix)))]
637 {
638 use std::os::unix::process::CommandExt as _;
639
640 let mut command = std::process::Command::new(executable);
641 command
642 .arg("__service")
643 .stdin(std::process::Stdio::null())
644 .stdout(std::process::Stdio::null())
645 .stderr(std::process::Stdio::null())
646 .process_group(0);
647 command.spawn().map(drop)
648 }
649}
650
651pub fn spawn_process_tree(command: &mut std::process::Command) -> io::Result<ProcessTree> {
658 ProcessTree::spawn(command)
659}
660
661pub struct NativeCompletion<T> {
664 state: Arc<Mutex<CompletionState<T>>>,
665 pending: Option<NativeJob>,
666 waiter: Option<u64>,
667 sequencer: Option<FileSequencer>,
668 uncertain: Option<Arc<AtomicBool>>,
669 predecessor: Option<Arc<OperationFence>>,
670 completion: Option<Arc<OperationFence>>,
671 terminated: bool,
672}
673
674pub type ReadBatch = NativeCompletion<Vec<Bytes>>;
676
677pub type UnitCompletion = NativeCompletion<()>;
679
680pub struct BlockingIoTask<T> {
684 state: Arc<Mutex<CompletionState<T>>>,
685 pending: Option<NativeJob>,
686 waiter: Option<u64>,
687 terminated: bool,
688}
689
690#[must_use]
693pub fn run_blocking_io<T: Send + 'static>(
694 operation: impl FnOnce() -> T + Send + 'static,
695) -> BlockingIoTask<T> {
696 let state = Arc::new(Mutex::new(CompletionState {
697 result: None,
698 waker: None,
699 }));
700 let completion = Arc::clone(&state);
701 BlockingIoTask {
702 pending: Some(NativeJob::Task(Box::new(move || {
703 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation))
704 .map_err(|_| io::Error::other("native blocking I/O task panicked"));
705 finish_job(&completion, result, None)
706 }))),
707 state,
708 waiter: None,
709 terminated: false,
710 }
711}
712
713#[derive(Default)]
714struct OperationFence {
715 state: Mutex<OperationFenceState>,
716 settled: Condvar,
717}
718
719#[derive(Default)]
720struct OperationFenceState {
721 complete: bool,
722 successor: Option<Waker>,
723 skipped_successor: Option<Arc<OperationFence>>,
724}
725
726impl OperationFence {
727 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
728 fn wait_until_complete(&self) {
729 let mut state = self
730 .state
731 .lock()
732 .unwrap_or_else(std::sync::PoisonError::into_inner);
733 while !state.complete {
734 state = self
735 .settled
736 .wait(state)
737 .unwrap_or_else(std::sync::PoisonError::into_inner);
738 }
739 }
740
741 #[cfg(all(test, any(windows, target_os = "linux")))]
742 fn is_complete(&self) -> bool {
743 self.state
744 .lock()
745 .unwrap_or_else(std::sync::PoisonError::into_inner)
746 .complete
747 }
748
749 fn complete_after(self: &Arc<Self>, predecessor: Option<Arc<Self>>) {
750 let Some(predecessor) = predecessor else {
751 self.complete();
752 return;
753 };
754 let mut state = predecessor
755 .state
756 .lock()
757 .unwrap_or_else(std::sync::PoisonError::into_inner);
758 if state.complete {
759 drop(state);
760 self.complete();
761 } else {
762 state.successor = None;
765 state.skipped_successor = Some(Arc::clone(self));
766 }
767 }
768
769 fn ready_or_register(&self, waker: &Waker) -> bool {
770 let mut state = self
771 .state
772 .lock()
773 .unwrap_or_else(std::sync::PoisonError::into_inner);
774 if state.complete {
775 true
776 } else {
777 state.successor = Some(waker.clone());
778 false
779 }
780 }
781
782 fn complete(&self) {
783 let mut next: Option<Arc<Self>> = None;
784 loop {
785 let current = next.as_deref().unwrap_or(self);
786 let mut state = current
787 .state
788 .lock()
789 .unwrap_or_else(std::sync::PoisonError::into_inner);
790 state.complete = true;
791 let successor = state.successor.take();
792 let following = state.skipped_successor.take();
793 drop(state);
794 current.settled.notify_all();
795 wake_capacity_waiter(successor);
796 let Some(following) = following else {
797 break;
798 };
799 next = Some(following);
800 }
801 }
802}
803
804struct CompletionState<T> {
805 result: Option<io::Result<T>>,
806 waker: Option<Waker>,
807}
808
809struct FileCompletion<T> {
813 state: Arc<Mutex<CompletionState<T>>>,
814 fence: Option<Arc<OperationFence>>,
815 uncertain: Option<Arc<AtomicBool>>,
816 _tail: Option<FileSequencer>,
817 file: Option<Arc<File>>,
818 finished: bool,
819}
820
821impl<T> FileCompletion<T> {
822 fn new(
823 state: Arc<Mutex<CompletionState<T>>>,
824 fence: Option<Arc<OperationFence>>,
825 uncertain: Option<Arc<AtomicBool>>,
826 tail: Option<FileSequencer>,
827 ) -> Self {
828 Self {
829 state,
830 fence,
831 uncertain,
832 _tail: tail,
833 file: None,
834 finished: false,
835 }
836 }
837
838 fn for_file(mut self, file: Arc<File>) -> Self {
839 self.file = Some(file);
840 self
841 }
842
843 fn finish(mut self, result: io::Result<T>) -> Option<Waker> {
844 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
845 if result.as_ref().err().is_some_and(is_uncertain_io_error)
846 && let (Some(file), Some(uncertain)) = (&self.file, &self.uncertain)
847 {
848 poison_file_health(file, uncertain);
849 }
850 let waker = finish_file_job(
851 &self.state,
852 result,
853 self.fence.take(),
854 self.uncertain.as_deref(),
855 );
856 self.finished = true;
857 waker
858 }
859}
860
861impl<T> Drop for FileCompletion<T> {
862 fn drop(&mut self) {
863 if !self.finished {
864 std::process::abort();
868 }
869 }
870}
871
872#[cfg(any(windows, target_os = "linux"))]
873fn submit_file_io<T: Send + 'static>(
874 completion: FileCompletion<T>,
875 submit: impl FnOnce(Box<dyn FnOnce(io::Result<T>) + Send>) -> io::Result<()>,
876) -> Option<Waker> {
877 let handoff = Arc::new(Mutex::new(Some(completion)));
882 let callback_handoff = Arc::clone(&handoff);
883 let callback = Box::new(move |result| {
884 let completion = callback_handoff
885 .lock()
886 .unwrap_or_else(std::sync::PoisonError::into_inner)
887 .take()
888 .unwrap_or_else(|| std::process::abort());
889 wake_capacity_waiter(completion.finish(result));
890 });
891 match submit(callback) {
892 Ok(()) => None,
893 Err(error) => handoff
894 .lock()
895 .unwrap_or_else(std::sync::PoisonError::into_inner)
896 .take()
897 .unwrap_or_else(|| std::process::abort())
898 .finish(Err(error)),
899 }
900}
901
902enum NativeJob {
903 Read {
904 file: Arc<File>,
905 reads: Vec<OwnedRead>,
906 #[cfg(windows)]
907 overlapped: bool,
908 tail: Option<FileSequencer>,
909 uncertain: Option<Arc<AtomicBool>>,
910 state: Arc<Mutex<CompletionState<Vec<Bytes>>>>,
911 completion: Option<Arc<OperationFence>>,
912 },
913 Unit {
914 file: Arc<File>,
915 operation: NativeUnitOperation,
916 #[cfg(windows)]
917 overlapped: bool,
918 tail: Option<FileSequencer>,
919 uncertain: Option<Arc<AtomicBool>>,
920 state: Arc<Mutex<CompletionState<()>>>,
921 completion: Option<Arc<OperationFence>>,
922 },
923 Task(Box<dyn FnOnce() -> Option<Waker> + Send>),
924}
925
926enum NativeUnitOperation {
927 Write(Vec<OwnedWrite>),
928 SetLen(u64),
929 Sync(Durability),
930 Control(FileControl),
931}
932
933type FileControl = Box<dyn FnOnce(&File) -> io::Result<()> + Send>;
934
935impl NativeJob {
936 fn set_completion(&mut self, fence: Arc<OperationFence>) {
937 match self {
938 Self::Read { completion, .. } | Self::Unit { completion, .. } => {
939 *completion = Some(fence);
940 }
941 Self::Task(_) => unreachable!("blocking tasks are not file-sequenced"),
942 }
943 }
944
945 fn run(self) -> Option<Waker> {
946 match self {
947 Self::Read {
948 file,
949 reads,
950 #[cfg(windows)]
951 overlapped,
952 tail,
953 uncertain,
954 state,
955 completion,
956 } => {
957 let finish = FileCompletion::new(state, completion, uncertain, tail)
958 .for_file(Arc::clone(&file));
959 #[cfg(target_os = "linux")]
960 return submit_file_io(finish, |callback| {
961 linux::submit_read(file, reads, callback)
962 });
963 #[cfg(windows)]
964 return submit_file_io(finish, |callback| {
965 windows::submit_read(file, overlapped, reads, callback)
966 });
967 #[cfg(not(any(windows, target_os = "linux")))]
968 {
969 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
970 read_batch_impl(&file, &reads)
971 }))
972 .unwrap_or_else(|_| std::process::abort());
977 finish.finish(result)
978 }
979 }
980 Self::Unit {
981 file,
982 operation,
983 #[cfg(windows)]
984 overlapped,
985 tail,
986 uncertain,
987 state,
988 completion,
989 } => {
990 let finish = FileCompletion::new(state, completion, uncertain, tail.clone())
991 .for_file(Arc::clone(&file));
992 #[cfg(any(windows, target_os = "linux"))]
993 let operation = match operation {
994 NativeUnitOperation::Write(writes) => {
995 if let Err(error) = validate_write_batch(&writes) {
996 return finish.finish(Err(error));
997 }
998 #[cfg(target_os = "linux")]
999 return submit_file_io(finish, |callback| {
1000 linux::submit_write(file, writes, callback)
1001 });
1002 #[cfg(windows)]
1003 return submit_file_io(finish, |callback| {
1004 windows::submit_write(file, overlapped, writes, callback)
1005 });
1006 }
1007 operation => operation,
1008 };
1009 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
1010 let _scope = tail.as_ref().map(FileScope::enter);
1011 let result =
1012 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| match operation {
1013 #[cfg(not(any(windows, target_os = "linux")))]
1014 NativeUnitOperation::Write(writes) => write_all_batch_owned(&file, writes),
1015 #[cfg(any(windows, target_os = "linux"))]
1016 NativeUnitOperation::Write(_) => {
1017 unreachable!("native writes are dispatched before bounded controls")
1018 }
1019 NativeUnitOperation::SetLen(size) => file.set_len(size),
1020 NativeUnitOperation::Sync(durability) => sync_file(&file, durability),
1021 NativeUnitOperation::Control(operation) => operation(&file),
1022 }))
1023 .unwrap_or_else(|_| std::process::abort());
1027 finish.finish(result)
1028 }
1029 Self::Task(operation) => operation(),
1030 }
1031 }
1032}
1033
1034fn finish_file_job<T>(
1035 state: &Mutex<CompletionState<T>>,
1036 result: io::Result<T>,
1037 completion: Option<Arc<OperationFence>>,
1038 uncertain: Option<&AtomicBool>,
1039) -> Option<Waker> {
1040 if result.as_ref().err().is_some_and(is_uncertain_io_error)
1041 && let Some(uncertain) = uncertain
1042 {
1043 uncertain.store(true, Ordering::Release);
1044 }
1045 finish_job(state, result, completion)
1046}
1047
1048fn finish_job<T>(
1049 state: &Mutex<CompletionState<T>>,
1050 result: io::Result<T>,
1051 completion: Option<Arc<OperationFence>>,
1052) -> Option<Waker> {
1053 let mut state = state
1054 .lock()
1055 .unwrap_or_else(std::sync::PoisonError::into_inner);
1056 state.result = Some(result);
1057 let waker = state.waker.take();
1058 drop(state);
1059 if let Some(completion) = completion {
1060 completion.complete();
1061 }
1062 waker
1063}
1064
1065struct NativeWorkers {
1066 sender: mpsc::SyncSender<NativeJob>,
1067 admission: Arc<Mutex<AdmissionState>>,
1068}
1069
1070struct AdmissionState {
1071 waiters: VecDeque<(u64, Waker)>,
1072 queued_jobs: usize,
1073 next_waiter: u64,
1074}
1075
1076impl AdmissionState {
1077 fn new() -> Self {
1078 Self {
1079 waiters: VecDeque::new(),
1080 queued_jobs: 0,
1081 next_waiter: 1,
1082 }
1083 }
1084
1085 fn next_waiter(&mut self) -> u64 {
1086 let id = self.next_waiter;
1087 self.next_waiter = id.wrapping_add(1).max(1);
1088 id
1089 }
1090}
1091
1092impl NativeCompletion<Vec<Bytes>> {
1093 fn submit(
1094 file: Arc<File>,
1095 reads: Vec<OwnedRead>,
1096 sequencer: Option<FileSequencer>,
1097 uncertain: Option<Arc<AtomicBool>>,
1098 #[cfg(windows)] overlapped: bool,
1099 ) -> Self {
1100 let state = Arc::new(Mutex::new(CompletionState {
1101 result: None,
1102 waker: None,
1103 }));
1104 Self {
1105 pending: Some(NativeJob::Read {
1106 file,
1107 reads,
1108 #[cfg(windows)]
1109 overlapped,
1110 tail: sequencer.clone(),
1111 uncertain: uncertain.clone(),
1112 state: Arc::clone(&state),
1113 completion: None,
1114 }),
1115 state,
1116 waiter: None,
1117 sequencer,
1118 uncertain,
1119 predecessor: None,
1120 completion: None,
1121 terminated: false,
1122 }
1123 }
1124}
1125
1126impl<T> NativeCompletion<T> {
1127 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
1128 fn failed(error: io::Error) -> Self {
1129 Self {
1130 state: Arc::new(Mutex::new(CompletionState {
1131 result: Some(Err(error)),
1132 waker: None,
1133 })),
1134 pending: None,
1135 waiter: None,
1136 sequencer: None,
1137 uncertain: None,
1138 predecessor: None,
1139 completion: None,
1140 terminated: false,
1141 }
1142 }
1143}
1144
1145impl NativeCompletion<()> {
1146 fn submit(
1147 file: Arc<File>,
1148 operation: NativeUnitOperation,
1149 sequencer: Option<FileSequencer>,
1150 uncertain: Option<Arc<AtomicBool>>,
1151 #[cfg(windows)] overlapped: bool,
1152 ) -> Self {
1153 let state = Arc::new(Mutex::new(CompletionState {
1154 result: None,
1155 waker: None,
1156 }));
1157 Self {
1158 pending: Some(NativeJob::Unit {
1159 file,
1160 operation,
1161 #[cfg(windows)]
1162 overlapped,
1163 tail: sequencer.clone(),
1164 uncertain: uncertain.clone(),
1165 state: Arc::clone(&state),
1166 completion: None,
1167 }),
1168 state,
1169 waiter: None,
1170 sequencer,
1171 uncertain,
1172 predecessor: None,
1173 completion: None,
1174 terminated: false,
1175 }
1176 }
1177}
1178
1179fn reserve_if_needed(
1180 sequencer: &mut Option<FileSequencer>,
1181 predecessor: &mut Option<Arc<OperationFence>>,
1182 completion: &mut Option<Arc<OperationFence>>,
1183 pending: &mut Option<NativeJob>,
1184) {
1185 let Some(sequencer) = sequencer.take() else {
1186 return;
1187 };
1188 let mut tail = sequencer
1189 .lock()
1190 .unwrap_or_else(std::sync::PoisonError::into_inner);
1191 *predecessor = tail.clone();
1192 let fence = Arc::new(OperationFence::default());
1193 if let Some(job) = pending {
1194 job.set_completion(Arc::clone(&fence));
1195 }
1196 *tail = Some(Arc::clone(&fence));
1197 *completion = Some(fence);
1198}
1199
1200impl<T> Future for NativeCompletion<T> {
1201 type Output = io::Result<T>;
1202
1203 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
1204 let this = self.get_mut();
1205 assert!(!this.terminated, "native I/O polled after completion");
1206 reserve_if_needed(
1207 &mut this.sequencer,
1208 &mut this.predecessor,
1209 &mut this.completion,
1210 &mut this.pending,
1211 );
1212 if let Some(predecessor) = &this.predecessor {
1213 if !predecessor.ready_or_register(context.waker()) {
1214 return Poll::Pending;
1215 }
1216 this.predecessor = None;
1217 }
1218 if this
1219 .uncertain
1220 .as_ref()
1221 .is_some_and(|uncertain| uncertain.load(Ordering::Acquire))
1222 {
1223 this.pending = None;
1224 if let Some(completion) = &this.completion {
1225 completion.complete();
1226 }
1227 this.terminated = true;
1228 return Poll::Ready(Err(io::Error::other(
1229 "prior native I/O completion on this file is uncertain",
1230 )));
1231 }
1232 match poll_submission(&mut this.pending, &mut this.waiter, context) {
1233 Poll::Ready(Err(error)) => {
1234 this.pending = None;
1235 if let Some(completion) = &this.completion {
1236 completion.complete();
1237 }
1238 this.terminated = true;
1239 return Poll::Ready(Err(error));
1240 }
1241 Poll::Ready(Ok(())) => {}
1242 Poll::Pending => return Poll::Pending,
1243 }
1244 let mut state = this
1245 .state
1246 .lock()
1247 .unwrap_or_else(std::sync::PoisonError::into_inner);
1248 if let Some(result) = state.result.take() {
1249 this.terminated = true;
1250 return Poll::Ready(result);
1251 }
1252 if !state
1253 .waker
1254 .as_ref()
1255 .is_some_and(|waker| waker.will_wake(context.waker()))
1256 {
1257 state.waker = Some(context.waker().clone());
1258 }
1259 Poll::Pending
1260 }
1261}
1262
1263impl<T> Future for BlockingIoTask<T> {
1264 type Output = io::Result<T>;
1265
1266 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
1267 let this = self.get_mut();
1268 assert!(!this.terminated, "host I/O task polled after completion");
1269 let workers = match blocking_workers() {
1270 Ok(workers) => workers,
1271 Err(error) => {
1272 this.terminated = true;
1273 return Poll::Ready(Err(error));
1274 }
1275 };
1276 match poll_submission_with(workers, &mut this.pending, &mut this.waiter, context) {
1277 Poll::Ready(Err(error)) => {
1278 this.terminated = true;
1279 return Poll::Ready(Err(error));
1280 }
1281 Poll::Ready(Ok(())) => {}
1282 Poll::Pending => return Poll::Pending,
1283 }
1284 let mut state = this
1285 .state
1286 .lock()
1287 .unwrap_or_else(std::sync::PoisonError::into_inner);
1288 if let Some(result) = state.result.take() {
1289 this.terminated = true;
1290 return Poll::Ready(result);
1291 }
1292 if !state
1293 .waker
1294 .as_ref()
1295 .is_some_and(|waker| waker.will_wake(context.waker()))
1296 {
1297 state.waker = Some(context.waker().clone());
1298 }
1299 Poll::Pending
1300 }
1301}
1302
1303fn poll_submission(
1304 pending: &mut Option<NativeJob>,
1305 waiter: &mut Option<u64>,
1306 context: &Context<'_>,
1307) -> Poll<io::Result<()>> {
1308 if pending.is_none() {
1309 return Poll::Ready(Ok(()));
1310 }
1311 let workers = match native_workers() {
1312 Ok(workers) => workers,
1313 Err(error) => return Poll::Ready(Err(error)),
1314 };
1315 poll_submission_with(workers, pending, waiter, context)
1316}
1317
1318fn poll_submission_with(
1319 workers: &NativeWorkers,
1320 pending: &mut Option<NativeJob>,
1321 waiter: &mut Option<u64>,
1322 context: &Context<'_>,
1323) -> Poll<io::Result<()>> {
1324 let Some(job) = pending.take() else {
1325 return Poll::Ready(Ok(()));
1326 };
1327 let mut admission = workers
1328 .admission
1329 .lock()
1330 .unwrap_or_else(std::sync::PoisonError::into_inner);
1331 let mut identity = *waiter;
1332 if let Some(identity) = identity {
1333 if let Some((_, waker)) = admission
1334 .waiters
1335 .iter_mut()
1336 .find(|(candidate, _)| *candidate == identity)
1337 {
1338 waker.clone_from(context.waker());
1339 } else {
1340 admission
1341 .waiters
1342 .push_back((identity, context.waker().clone()));
1343 }
1344 if admission.queued_jobs != 0
1345 && admission
1346 .waiters
1347 .front()
1348 .is_some_and(|(front, _)| *front != identity)
1349 {
1350 *pending = Some(job);
1351 return Poll::Pending;
1352 }
1353 } else if admission.queued_jobs != 0 && !admission.waiters.is_empty() {
1354 let queued = admission.next_waiter();
1355 *waiter = Some(queued);
1356 admission
1357 .waiters
1358 .push_back((queued, context.waker().clone()));
1359 *pending = Some(job);
1360 return Poll::Pending;
1361 }
1362 match workers.sender.try_send(job) {
1363 Ok(()) => {
1364 admission.queued_jobs += 1;
1367 let next = identity.and_then(|submitted| {
1368 waiter.take();
1369 let was_front = admission
1370 .waiters
1371 .front()
1372 .is_some_and(|(front, _)| *front == submitted);
1373 admission
1374 .waiters
1375 .retain(|(candidate, _)| *candidate != submitted);
1376 was_front
1377 .then(|| admission.waiters.front().map(|(_, waker)| waker.clone()))
1378 .flatten()
1379 });
1380 drop(admission);
1381 wake_capacity_waiter(next);
1382 Poll::Ready(Ok(()))
1383 }
1384 Err(mpsc::TrySendError::Full(job)) => {
1385 if identity.is_none() {
1386 let queued = admission.next_waiter();
1387 *waiter = Some(queued);
1388 identity = Some(queued);
1389 admission
1390 .waiters
1391 .push_back((queued, context.waker().clone()));
1392 }
1393 debug_assert!(identity.is_some());
1394 *pending = Some(job);
1395 Poll::Pending
1396 }
1397 Err(mpsc::TrySendError::Disconnected(_)) => {
1398 let next = identity.and_then(|submitted| {
1399 waiter.take();
1400 let was_front = admission
1401 .waiters
1402 .front()
1403 .is_some_and(|(front, _)| *front == submitted);
1404 admission
1405 .waiters
1406 .retain(|(candidate, _)| *candidate != submitted);
1407 was_front
1408 .then(|| admission.waiters.front().map(|(_, waker)| waker.clone()))
1409 .flatten()
1410 });
1411 drop(admission);
1412 wake_capacity_waiter(next);
1413 Poll::Ready(Err(io::Error::new(
1414 io::ErrorKind::BrokenPipe,
1415 "native I/O workers stopped",
1416 )))
1417 }
1418 }
1419}
1420
1421fn remove_capacity_waiter(workers: &NativeWorkers, waiter: Option<u64>) {
1422 let Some(waiter) = waiter else {
1423 return;
1424 };
1425 let mut admission = workers
1426 .admission
1427 .lock()
1428 .unwrap_or_else(std::sync::PoisonError::into_inner);
1429 let was_front = admission
1430 .waiters
1431 .front()
1432 .is_some_and(|(identity, _)| *identity == waiter);
1433 admission
1434 .waiters
1435 .retain(|(identity, _)| *identity != waiter);
1436 let next = was_front
1437 .then(|| admission.waiters.front().map(|(_, waker)| waker.clone()))
1438 .flatten();
1439 drop(admission);
1440 wake_capacity_waiter(next);
1441}
1442
1443fn wake_capacity_waiter(waiter: Option<Waker>) {
1444 if let Some(waiter) = waiter {
1445 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| waiter.wake()));
1446 }
1447}
1448
1449fn notify_capacity_released(admission: &Mutex<AdmissionState>) {
1450 let (first, others) = {
1451 let mut admission = admission
1452 .lock()
1453 .unwrap_or_else(std::sync::PoisonError::into_inner);
1454 debug_assert_ne!(admission.queued_jobs, 0, "native queue accounting lost");
1455 admission.queued_jobs = admission.queued_jobs.saturating_sub(1);
1456 let first = admission.waiters.front().map(|(_, waker)| waker.clone());
1457 let others = if admission.queued_jobs == 0 {
1461 admission
1462 .waiters
1463 .iter()
1464 .skip(1)
1465 .map(|(_, waker)| waker.clone())
1466 .collect()
1467 } else {
1468 Vec::new()
1469 };
1470 (first, others)
1471 };
1472 wake_capacity_waiter(first);
1473 for waiter in others {
1474 wake_capacity_waiter(Some(waiter));
1475 }
1476}
1477
1478fn native_workers() -> io::Result<&'static NativeWorkers> {
1479 static WORKERS: OnceLock<io::Result<NativeWorkers>> = OnceLock::new();
1480 let parallelism = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
1481 workers(&WORKERS, "acyclic-native-io", parallelism.min(4))
1482}
1483
1484fn blocking_workers() -> io::Result<&'static NativeWorkers> {
1485 static WORKERS: OnceLock<io::Result<NativeWorkers>> = OnceLock::new();
1486 let parallelism = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
1487 workers(
1488 &WORKERS,
1489 "acyclic-host-io",
1490 parallelism.saturating_mul(2).clamp(2, 16),
1491 )
1492}
1493
1494fn workers(
1495 slot: &'static OnceLock<io::Result<NativeWorkers>>,
1496 name: &str,
1497 worker_count: usize,
1498) -> io::Result<&'static NativeWorkers> {
1499 slot.get_or_init(|| {
1500 let (sender, receiver) = mpsc::sync_channel::<NativeJob>(worker_count * 4);
1501 let receiver = Arc::new(Mutex::new(receiver));
1502 let admission = Arc::new(Mutex::new(AdmissionState::new()));
1503 for worker_index in 0..worker_count {
1504 let receiver = Arc::clone(&receiver);
1505 let admission = Arc::clone(&admission);
1506 std::thread::Builder::new()
1507 .name(format!("{name}-{worker_index}"))
1508 .spawn(move || {
1509 loop {
1510 let job = {
1511 let receiver = receiver
1512 .lock()
1513 .unwrap_or_else(std::sync::PoisonError::into_inner);
1514 receiver.recv()
1515 };
1516 let Ok(job) = job else {
1517 break;
1518 };
1519 notify_capacity_released(&admission);
1520 let waker = job.run();
1521 if let Some(waker) = waker {
1522 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1523 waker.wake();
1524 }));
1525 }
1526 }
1527 })?;
1528 }
1529 Ok(NativeWorkers { sender, admission })
1530 })
1531 .as_ref()
1532 .map_err(|error| io::Error::new(error.kind(), error.to_string()))
1533}
1534
1535impl<T> Drop for NativeCompletion<T> {
1536 fn drop(&mut self) {
1537 if self.pending.is_some()
1538 && let Some(completion) = &self.completion
1539 {
1540 completion.complete_after(self.predecessor.take());
1541 }
1542 self.state
1543 .lock()
1544 .unwrap_or_else(std::sync::PoisonError::into_inner)
1545 .waker = None;
1546 let Some(waiter) = self.waiter else {
1547 return;
1548 };
1549 let Ok(workers) = native_workers() else {
1550 return;
1551 };
1552 remove_capacity_waiter(workers, Some(waiter));
1553 }
1554}
1555
1556impl<T> Drop for BlockingIoTask<T> {
1557 fn drop(&mut self) {
1558 self.state
1559 .lock()
1560 .unwrap_or_else(std::sync::PoisonError::into_inner)
1561 .waker = None;
1562 let Some(waiter) = self.waiter else {
1563 return;
1564 };
1565 let Ok(workers) = blocking_workers() else {
1566 return;
1567 };
1568 remove_capacity_waiter(workers, Some(waiter));
1569 }
1570}
1571
1572pub struct RangeReader<'a> {
1574 file: &'a File,
1575 offset: u64,
1576 remaining: u64,
1577}
1578
1579pub struct AsyncRangeReader {
1581 file: NativeFile,
1582 offset: u64,
1583 remaining: u64,
1584}
1585
1586impl AsyncRangeReader {
1587 pub fn new(file: File, offset: u64, length: u64) -> io::Result<Self> {
1589 Ok(Self {
1590 file: NativeFile::from_file(file)?,
1591 offset,
1592 remaining: length,
1593 })
1594 }
1595
1596 pub async fn read(&mut self, maximum: usize) -> io::Result<Bytes> {
1598 let length = usize::try_from(self.remaining.min(maximum as u64))
1599 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "range too large"))?;
1600 if length == 0 {
1601 return Ok(Bytes::new());
1602 }
1603 let mut result = self
1604 .file
1605 .read_batch_async(vec![OwnedRead {
1606 offset: self.offset,
1607 length,
1608 }])
1609 .await?;
1610 let bytes = result
1611 .pop()
1612 .ok_or_else(|| io::Error::other("native read returned no result"))?;
1613 self.offset = self
1614 .offset
1615 .checked_add(bytes.len() as u64)
1616 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "read offset overflow"))?;
1617 self.remaining -= bytes.len() as u64;
1618 Ok(bytes)
1619 }
1620}
1621
1622impl<'a> RangeReader<'a> {
1623 pub const fn new(file: &'a File, offset: u64, length: u64) -> Self {
1625 Self {
1626 file,
1627 offset,
1628 remaining: length,
1629 }
1630 }
1631}
1632
1633impl Read for RangeReader<'_> {
1634 fn read(&mut self, destination: &mut [u8]) -> io::Result<usize> {
1635 let maximum = usize::try_from(self.remaining.min(destination.len() as u64))
1636 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "range too large"))?;
1637 if maximum == 0 {
1638 return Ok(0);
1639 }
1640 let selected = destination.get_mut(..maximum).ok_or_else(|| {
1641 io::Error::new(io::ErrorKind::InvalidInput, "range exceeds destination")
1642 })?;
1643 let count = read_at(self.file, self.offset, selected)?;
1644 self.offset = self
1645 .offset
1646 .checked_add(count as u64)
1647 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "read offset overflow"))?;
1648 self.remaining -= count as u64;
1649 Ok(count)
1650 }
1651}
1652
1653#[cfg(target_os = "linux")]
1654fn read_at_impl(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
1655 linux::read_at(file, offset, destination)
1656}
1657
1658#[cfg(target_os = "linux")]
1659fn write_all_at_impl(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
1660 linux::write_all_at(file, offset, bytes)
1661}
1662
1663#[cfg(not(any(windows, target_os = "linux")))]
1664fn write_all_batch_owned(file: &Arc<File>, writes: Vec<OwnedWrite>) -> io::Result<()> {
1665 validate_write_batch(&writes)?;
1666 write_all_batch_impl(file, writes)
1667}
1668
1669fn validate_write_batch(writes: &[OwnedWrite]) -> io::Result<()> {
1670 let mut ranges = Vec::new();
1671 ranges.try_reserve_exact(writes.len())?;
1672 for write in writes {
1673 if write.offset == u64::MAX {
1674 return Err(io::Error::new(
1675 io::ErrorKind::InvalidInput,
1676 "batch write offset aliases the shared file cursor",
1677 ));
1678 }
1679 let length = u64::try_from(write.bytes.len()).map_err(|_| {
1680 io::Error::new(io::ErrorKind::InvalidInput, "batch write length is invalid")
1681 })?;
1682 let end = write.offset.checked_add(length).ok_or_else(|| {
1683 io::Error::new(io::ErrorKind::InvalidInput, "batch write range overflows")
1684 })?;
1685 if !write.bytes.is_empty() {
1686 ranges.push((write.offset, end));
1687 }
1688 }
1689 ranges.sort_unstable();
1690 if ranges
1691 .windows(2)
1692 .any(|pair| matches!(pair, [first, second] if first.1 > second.0))
1693 {
1694 return Err(io::Error::new(
1695 io::ErrorKind::InvalidInput,
1696 "overlapping batch write ranges",
1697 ));
1698 }
1699 Ok(())
1700}
1701
1702#[cfg(all(unix, not(target_os = "linux")))]
1703fn read_at_impl(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
1704 use std::os::unix::fs::FileExt as _;
1705 file.read_at(destination, offset)
1706}
1707
1708#[cfg(all(unix, not(target_os = "linux")))]
1709fn write_all_at_impl(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
1710 use std::os::unix::fs::FileExt as _;
1711 file.write_all_at(bytes, offset)
1712}
1713
1714#[cfg(all(unix, not(target_os = "linux")))]
1715fn write_all_batch_impl(file: &File, writes: Vec<OwnedWrite>) -> io::Result<()> {
1716 apple::write_all_batch_owned(file, writes)
1717}
1718
1719#[cfg(all(unix, not(target_os = "linux")))]
1720fn read_batch_impl(file: &File, reads: &[OwnedRead]) -> io::Result<Vec<Bytes>> {
1721 apple::read_batch(file, reads)
1722}
1723
1724#[cfg(windows)]
1725fn wait_for_windows_io<T: Send + 'static>(
1726 submit: impl FnOnce(Box<dyn FnOnce(io::Result<T>) + Send>) -> io::Result<()>,
1727) -> io::Result<T> {
1728 let (sender, receiver) = mpsc::sync_channel(1);
1729 submit(Box::new(move |result| {
1730 let _ = sender.send(result);
1731 }))?;
1732 receiver.recv().map_err(|_| {
1733 io::Error::new(
1734 io::ErrorKind::BrokenPipe,
1735 "completion owner stopped before terminal I/O result",
1736 )
1737 })?
1738}
1739
1740#[cfg(windows)]
1741fn read_at_impl(file: &File, offset: u64, destination: &mut [u8]) -> io::Result<usize> {
1742 let file = Arc::new(file.try_clone()?);
1743 let mut reads = wait_for_windows_io(|finish| {
1744 windows::submit_read(
1745 file,
1746 false,
1747 vec![OwnedRead {
1748 offset,
1749 length: destination.len(),
1750 }],
1751 finish,
1752 )
1753 })?;
1754 let bytes = reads
1755 .pop()
1756 .ok_or_else(|| io::Error::other("native read returned no result"))?;
1757 destination
1758 .get_mut(..bytes.len())
1759 .ok_or_else(|| io::Error::other("native read exceeded submitted length"))?
1760 .copy_from_slice(&bytes);
1761 Ok(bytes.len())
1762}
1763
1764#[cfg(windows)]
1765fn write_all_at_impl(file: &File, offset: u64, bytes: &[u8]) -> io::Result<()> {
1766 let file = Arc::new(file.try_clone()?);
1767 wait_for_windows_io(|finish| {
1768 windows::submit_write(
1769 file,
1770 false,
1771 vec![OwnedWrite {
1772 offset,
1773 bytes: Bytes::copy_from_slice(bytes),
1774 }],
1775 finish,
1776 )
1777 })
1778}
1779
1780#[cfg(target_vendor = "apple")]
1781#[allow(unsafe_code)]
1782fn barrier_sync(file: &File) -> io::Result<()> {
1783 use std::os::fd::AsRawFd as _;
1784 if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_BARRIERFSYNC) } == 0 {
1786 Ok(())
1787 } else {
1788 Err(io::Error::last_os_error())
1789 }
1790}
1791
1792#[cfg(not(target_vendor = "apple"))]
1793fn barrier_sync(_file: &File) -> io::Result<()> {
1794 Err(io::Error::new(
1795 io::ErrorKind::Unsupported,
1796 "ordered write barrier is unavailable on this platform",
1797 ))
1798}
1799
1800#[cfg(unix)]
1801fn sync_parent_impl(path: &Path, durability: Durability) -> io::Result<()> {
1802 sync_file(&File::open(path)?, durability)
1803}
1804
1805#[cfg(windows)]
1806fn sync_parent_impl(path: &Path, durability: Durability) -> io::Result<()> {
1807 use std::fs::OpenOptions;
1808 use std::os::windows::fs::OpenOptionsExt as _;
1809
1810 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
1811 const FILE_SHARE_READ_WRITE_DELETE: u32 = 0x0000_0001 | 0x0000_0002 | 0x0000_0004;
1812 let directory = OpenOptions::new()
1813 .read(true)
1814 .write(true)
1815 .share_mode(FILE_SHARE_READ_WRITE_DELETE)
1816 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
1817 .open(path)?;
1818 sync_file(&directory, durability)
1819}
1820
1821#[cfg(target_os = "linux")]
1822#[allow(unsafe_code, reason = "renameat2 receives two live C paths")]
1823fn durable_rename_impl(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
1824 use std::os::unix::ffi::OsStrExt as _;
1825 let from_path = std::ffi::CString::new(from.as_os_str().as_bytes())
1826 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
1827 let to_path = std::ffi::CString::new(to.as_os_str().as_bytes())
1828 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
1829 let flags = match mode {
1830 RenameMode::NoReplace => libc::RENAME_NOREPLACE,
1831 RenameMode::Replace => 0,
1832 };
1833 let result = unsafe {
1834 libc::syscall(
1835 libc::SYS_renameat2,
1836 libc::AT_FDCWD,
1837 from_path.as_ptr(),
1838 libc::AT_FDCWD,
1839 to_path.as_ptr(),
1840 flags,
1841 )
1842 };
1843 if result != 0 {
1844 return Err(io::Error::last_os_error());
1845 }
1846 sync_rename_parents(from, to)
1847}
1848
1849#[cfg(target_os = "macos")]
1850#[allow(unsafe_code, reason = "renamex_np receives two live C paths")]
1851fn durable_rename_impl(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
1852 use std::os::unix::ffi::OsStrExt as _;
1853 let from_path = std::ffi::CString::new(from.as_os_str().as_bytes())
1854 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
1855 let to_path = std::ffi::CString::new(to.as_os_str().as_bytes())
1856 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "rename path contains NUL"))?;
1857 let flags = match mode {
1858 RenameMode::NoReplace => libc::RENAME_EXCL,
1859 RenameMode::Replace => 0,
1860 };
1861 if unsafe { libc::renamex_np(from_path.as_ptr(), to_path.as_ptr(), flags) } != 0 {
1862 return Err(io::Error::last_os_error());
1863 }
1864 sync_rename_parents(from, to)
1865}
1866
1867#[cfg(unix)]
1868fn sync_rename_parents(from: &Path, to: &Path) -> io::Result<()> {
1869 fn namespace_parent(path: &Path) -> &Path {
1870 path.parent()
1871 .filter(|parent| !parent.as_os_str().is_empty())
1872 .unwrap_or_else(|| Path::new("."))
1873 }
1874 let to_parent = namespace_parent(to);
1875 sync_parent(to_parent, Durability::Full)?;
1876 let from_parent = namespace_parent(from);
1877 if from_parent != to_parent {
1878 sync_parent(from_parent, Durability::Full)?;
1879 }
1880 Ok(())
1881}
1882
1883#[cfg(windows)]
1884#[allow(unsafe_code, reason = "MoveFileExW receives terminated UTF-16 paths")]
1885fn durable_rename_impl(from: &Path, to: &Path, mode: RenameMode) -> io::Result<()> {
1886 use std::os::windows::ffi::OsStrExt as _;
1887 use windows_sys::Win32::Storage::FileSystem::{
1888 MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW,
1889 };
1890 let mut from: Vec<u16> = from.as_os_str().encode_wide().collect();
1891 let mut to: Vec<u16> = to.as_os_str().encode_wide().collect();
1892 if from.contains(&0) || to.contains(&0) {
1893 return Err(io::Error::new(
1894 io::ErrorKind::InvalidInput,
1895 "rename path contains NUL",
1896 ));
1897 }
1898 from.push(0);
1899 to.push(0);
1900 let flags = MOVEFILE_WRITE_THROUGH
1901 | if mode == RenameMode::Replace {
1902 MOVEFILE_REPLACE_EXISTING
1903 } else {
1904 0
1905 };
1906 if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), flags) } == 0 {
1908 Err(io::Error::last_os_error())
1909 } else {
1910 Ok(())
1911 }
1912}
1913
1914#[cfg(test)]
1915mod tests {
1916 use super::*;
1917 use std::fs::OpenOptions;
1918 use std::io::{Seek as _, SeekFrom};
1919 use std::sync::Arc;
1920 use std::sync::atomic::{AtomicU64, Ordering};
1921 use std::task::{Poll, Wake};
1922 use std::thread::Thread;
1923 use std::time::{Duration, Instant};
1924
1925 #[cfg(any(windows, target_os = "linux"))]
1926 #[test]
1927 fn platform_submission_transfers_completion_only_after_acceptance() -> io::Result<()> {
1928 let state = Arc::new(Mutex::new(CompletionState::<usize> {
1929 result: None,
1930 waker: None,
1931 }));
1932 let fence = Arc::new(OperationFence::default());
1933 let uncertain = Arc::new(AtomicBool::new(false));
1934 let mut callback = None;
1935 let wake = submit_file_io(
1936 FileCompletion::new(
1937 Arc::clone(&state),
1938 Some(Arc::clone(&fence)),
1939 Some(Arc::clone(&uncertain)),
1940 None,
1941 ),
1942 |finish| {
1943 callback = Some(finish);
1944 Ok(())
1945 },
1946 );
1947 assert!(wake.is_none());
1948 assert!(!fence.is_complete());
1949 callback
1950 .take()
1951 .ok_or_else(|| io::Error::other("accepted callback missing"))?(Ok(7));
1952 assert!(fence.is_complete());
1953 let actual = state
1954 .lock()
1955 .map_err(|_| io::Error::other("completion state poisoned"))?
1956 .result
1957 .take()
1958 .ok_or_else(|| io::Error::other("accepted result missing"))??;
1959 assert_eq!(actual, 7);
1960 assert!(!uncertain.load(Ordering::Acquire));
1961
1962 let rejected = Arc::new(Mutex::new(CompletionState::<usize> {
1963 result: None,
1964 waker: None,
1965 }));
1966 let rejected_fence = Arc::new(OperationFence::default());
1967 assert!(
1968 submit_file_io(
1969 FileCompletion::new(
1970 Arc::clone(&rejected),
1971 Some(Arc::clone(&rejected_fence)),
1972 None,
1973 None,
1974 ),
1975 |_finish| Err(io::Error::other("not submitted")),
1976 )
1977 .is_none()
1978 );
1979 assert!(rejected_fence.is_complete());
1980 let rejected_result = rejected
1981 .lock()
1982 .map_err(|_| io::Error::other("rejected completion state poisoned"))?
1983 .result
1984 .take()
1985 .ok_or_else(|| io::Error::other("rejected result missing"))?;
1986 assert!(matches!(rejected_result, Err(error) if error.to_string() == "not submitted"));
1987 Ok(())
1988 }
1989
1990 #[cfg(any(windows, target_os = "linux"))]
1991 #[test]
1992 fn accepted_callback_cannot_disappear_without_terminal_completion() -> io::Result<()> {
1993 const MARKER: &str = "ACYCLIC_DROPPED_COMPLETION_CHILD";
1994 if std::env::var_os(MARKER).is_some() {
1995 let state = Arc::new(Mutex::new(CompletionState::<()> {
1996 result: None,
1997 waker: None,
1998 }));
1999 eprintln!("dropping accepted native callback");
2000 let _ = submit_file_io(FileCompletion::new(state, None, None, None), |callback| {
2001 drop(callback);
2002 Ok(())
2003 });
2004 return Err(io::Error::other("missing terminal callback was accepted"));
2005 }
2006 let output = std::process::Command::new(std::env::current_exe()?)
2007 .arg("--exact")
2008 .arg("tests::accepted_callback_cannot_disappear_without_terminal_completion")
2009 .arg("--nocapture")
2010 .env(MARKER, "1")
2011 .output()?;
2012 assert!(!output.status.success());
2013 assert!(
2014 String::from_utf8_lossy(&output.stderr).contains("dropping accepted native callback")
2015 );
2016 Ok(())
2017 }
2018
2019 #[test]
2020 fn native_file_worker_unwind_is_fail_stop() -> io::Result<()> {
2021 const CHILD_MARKER: &str = "ACYCLIC_NATIVE_UNWIND_CHILD_TEST";
2022 if std::env::var_os(CHILD_MARKER).is_some() {
2023 let directory = tempfile::tempdir()?;
2024 let file = NativeFile::from_file(
2025 OpenOptions::new()
2026 .create_new(true)
2027 .read(true)
2028 .write(true)
2029 .open(directory.path().join("panic-worker"))?,
2030 )?;
2031 let _ = complete_write(file.control_async(|_| {
2032 eprintln!("injected native worker unwind");
2033 std::panic::resume_unwind(Box::new("injected native worker unwind"));
2034 }));
2035 return Err(io::Error::other(
2036 "native worker unwind returned to observer",
2037 ));
2038 }
2039 let output = std::process::Command::new(std::env::current_exe()?)
2040 .arg("--exact")
2041 .arg("tests::native_file_worker_unwind_is_fail_stop")
2042 .arg("--nocapture")
2043 .env(CHILD_MARKER, "1")
2044 .output()?;
2045 assert!(
2046 !output.status.success(),
2047 "native worker unwind escaped fail-stop"
2048 );
2049 assert!(
2050 String::from_utf8_lossy(&output.stderr).contains("injected native worker unwind"),
2051 "child did not reach the injected worker panic"
2052 );
2053 Ok(())
2054 }
2055
2056 struct ThreadWake(Thread);
2057
2058 impl Wake for ThreadWake {
2059 fn wake(self: Arc<Self>) {
2060 self.0.unpark();
2061 }
2062
2063 fn wake_by_ref(self: &Arc<Self>) {
2064 self.0.unpark();
2065 }
2066 }
2067
2068 fn completion_waker() -> Waker {
2069 Waker::from(Arc::new(ThreadWake(std::thread::current())))
2070 }
2071
2072 #[cfg(unix)]
2073 fn direct_write_at(file: &File, bytes: &[u8], offset: u64) -> io::Result<()> {
2074 use std::os::unix::fs::FileExt as _;
2075 file.write_all_at(bytes, offset)
2076 }
2077
2078 #[cfg(windows)]
2079 fn direct_write_at(file: &File, mut bytes: &[u8], mut offset: u64) -> io::Result<()> {
2080 use std::os::windows::fs::FileExt as _;
2081 while !bytes.is_empty() {
2082 let count = file.seek_write(bytes, offset)?;
2083 if count == 0 {
2084 return Err(io::Error::new(
2085 io::ErrorKind::WriteZero,
2086 "direct write returned zero",
2087 ));
2088 }
2089 offset = offset.checked_add(count as u64).ok_or_else(|| {
2090 io::Error::new(io::ErrorKind::InvalidInput, "write offset overflow")
2091 })?;
2092 bytes = bytes
2093 .get(count..)
2094 .ok_or_else(|| io::Error::other("overlong direct write"))?;
2095 }
2096 Ok(())
2097 }
2098
2099 #[cfg(unix)]
2100 fn direct_read_at(file: &File, bytes: &mut [u8], offset: u64) -> io::Result<usize> {
2101 use std::os::unix::fs::FileExt as _;
2102 file.read_at(bytes, offset)
2103 }
2104
2105 #[cfg(windows)]
2106 fn direct_read_at(file: &File, bytes: &mut [u8], offset: u64) -> io::Result<usize> {
2107 use std::os::windows::fs::FileExt as _;
2108 file.seek_read(bytes, offset)
2109 }
2110
2111 #[test]
2112 fn exclusive_lock_contention_is_not_a_generic_io_failure() {
2113 assert!(is_exclusive_lock_contention(&io::Error::from(
2114 io::ErrorKind::WouldBlock
2115 )));
2116 assert!(!is_exclusive_lock_contention(&io::Error::from(
2117 io::ErrorKind::PermissionDenied
2118 )));
2119 assert!(!is_exclusive_lock_contention(&io::Error::from(
2120 io::ErrorKind::Unsupported
2121 )));
2122 #[cfg(windows)]
2123 assert!(is_exclusive_lock_contention(&io::Error::from_raw_os_error(
2124 33
2125 )));
2126 }
2127
2128 #[test]
2129 fn ownership_release_barrier_waits_for_the_final_clone() {
2130 let anchor = OwnershipAnchor::new(());
2131 let clone = anchor.clone();
2132 let barrier = anchor.release_barrier();
2133 drop(anchor);
2134 assert!(!barrier.is_released());
2135 drop(clone);
2136 barrier.wait();
2137 }
2138
2139 #[test]
2140 fn durable_rename_moves_and_replaces_entries() -> io::Result<()> {
2141 let directory = tempfile::tempdir()?;
2142 let source = directory.path().join("source");
2143 let destination = directory.path().join("destination");
2144 std::fs::write(&source, b"first")?;
2145 durable_rename(&source, &destination, RenameMode::NoReplace)?;
2146 assert!(!source.exists());
2147 assert_eq!(std::fs::read(&destination)?, b"first");
2148
2149 std::fs::write(&source, b"second")?;
2150 durable_rename(&source, &destination, RenameMode::Replace)?;
2151 assert!(!source.exists());
2152 assert_eq!(std::fs::read(destination)?, b"second");
2153 Ok(())
2154 }
2155
2156 #[cfg(unix)]
2157 #[test]
2158 fn durable_rename_accepts_relative_sibling_paths() -> io::Result<()> {
2159 let temporary = tempfile::tempdir()?;
2160 let original = std::env::current_dir()?;
2161 std::env::set_current_dir(temporary.path())?;
2162 let result = (|| {
2163 std::fs::write("from", b"relative")?;
2164 durable_rename(Path::new("from"), Path::new("to"), RenameMode::NoReplace)?;
2165 assert_eq!(std::fs::read("to")?, b"relative");
2166 Ok(())
2167 })();
2168 std::env::set_current_dir(original)?;
2169 result
2170 }
2171
2172 #[test]
2173 fn no_replace_preserves_both_entries() -> io::Result<()> {
2174 let directory = tempfile::tempdir()?;
2175 let source = directory.path().join("source");
2176 let destination = directory.path().join("destination");
2177 std::fs::write(&source, b"source")?;
2178 std::fs::write(&destination, b"destination")?;
2179 let Err(error) = durable_rename(&source, &destination, RenameMode::NoReplace) else {
2180 return Err(io::Error::other(
2181 "existing destination accepted no-replace rename",
2182 ));
2183 };
2184 assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
2185 assert_eq!(std::fs::read(source)?, b"source");
2186 assert_eq!(std::fs::read(destination)?, b"destination");
2187 Ok(())
2188 }
2189
2190 fn complete_read(mut read: ReadBatch) -> io::Result<Vec<Bytes>> {
2191 let deadline = Instant::now() + Duration::from_secs(10);
2192 let waker = completion_waker();
2193 loop {
2194 match Pin::new(&mut read).poll(&mut Context::from_waker(&waker)) {
2195 Poll::Ready(result) => return result,
2196 Poll::Pending if Instant::now() < deadline => {
2197 std::thread::park_timeout(deadline.saturating_duration_since(Instant::now()));
2198 }
2199 Poll::Pending => {
2200 return Err(io::Error::new(
2201 io::ErrorKind::TimedOut,
2202 "owned native read did not complete",
2203 ));
2204 }
2205 }
2206 }
2207 }
2208
2209 fn complete_write(mut write: UnitCompletion) -> io::Result<()> {
2210 let deadline = Instant::now() + Duration::from_secs(10);
2211 let waker = completion_waker();
2212 loop {
2213 match Pin::new(&mut write).poll(&mut Context::from_waker(&waker)) {
2214 Poll::Ready(result) => return result,
2215 Poll::Pending if Instant::now() < deadline => {
2216 std::thread::park_timeout(deadline.saturating_duration_since(Instant::now()));
2217 }
2218 Poll::Pending => {
2219 return Err(io::Error::new(
2220 io::ErrorKind::TimedOut,
2221 "owned native write did not complete",
2222 ));
2223 }
2224 }
2225 }
2226 }
2227
2228 #[test]
2229 fn completed_native_futures_reject_repoll() -> io::Result<()> {
2230 fn finish<T>(future: &mut (impl Future<Output = io::Result<T>> + Unpin)) -> io::Result<T> {
2231 let deadline = Instant::now() + Duration::from_secs(10);
2232 let waker = completion_waker();
2233 loop {
2234 match Pin::new(&mut *future).poll(&mut Context::from_waker(&waker)) {
2235 Poll::Ready(result) => return result,
2236 Poll::Pending if Instant::now() < deadline => {
2237 std::thread::park_timeout(
2238 deadline.saturating_duration_since(Instant::now()),
2239 );
2240 }
2241 Poll::Pending => {
2242 return Err(io::Error::new(
2243 io::ErrorKind::TimedOut,
2244 "native future stalled",
2245 ));
2246 }
2247 }
2248 }
2249 }
2250
2251 fn repoll_panics(future: &mut (impl Future + Unpin)) {
2252 assert!(
2253 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2254 let _ = Pin::new(future).poll(&mut Context::from_waker(Waker::noop()));
2255 }))
2256 .is_err()
2257 );
2258 }
2259
2260 let temporary = tempfile::tempdir()?;
2261 let file = OpenOptions::new()
2262 .create_new(true)
2263 .read(true)
2264 .write(true)
2265 .open(temporary.path().join("completed-native-futures"))?;
2266 let mut read = read_batch_async(file.try_clone()?, vec![]);
2267 assert!(finish(&mut read)?.is_empty());
2268 repoll_panics(&mut read);
2269
2270 let mut write = write_all_batch_async(file, vec![]);
2271 finish(&mut write)?;
2272 repoll_panics(&mut write);
2273
2274 let mut host_task = run_blocking_io(|| 7);
2275 assert_eq!(finish(&mut host_task)?, 7);
2276 repoll_panics(&mut host_task);
2277 Ok(())
2278 }
2279
2280 #[test]
2281 fn positional_io_preserves_cursor_and_bounds_ranges() -> io::Result<()> {
2282 let temporary = tempfile::tempdir()?;
2283 let path = temporary.path().join("positional");
2284 let file = OpenOptions::new()
2285 .create_new(true)
2286 .read(true)
2287 .write(true)
2288 .open(path)?;
2289 write_all_at(&file, 2, &[0x22; 3])?;
2290 write_all_at(&file, 16, &[0x33; 5])?;
2291 sync_file(&file, Durability::Full)?;
2292
2293 let mut bytes = [0_u8; 24];
2294 assert_eq!(read_at(&file, 0, &mut bytes)?, 21);
2295 assert_eq!(&bytes[..8], &[0, 0, 0x22, 0x22, 0x22, 0, 0, 0]);
2296 assert_eq!(&bytes[8..16], &[0; 8]);
2297 assert_eq!(&bytes[16..21], &[0x33; 5]);
2298 assert_eq!(read_at(&file, 21, &mut bytes)?, 0);
2299
2300 let mut range = RangeReader::new(&file, 15, 8);
2301 let mut selected = Vec::new();
2302 range.read_to_end(&mut selected)?;
2303 assert_eq!(selected, [0, 0x33, 0x33, 0x33, 0x33, 0x33]);
2304
2305 complete_write(write_all_batch_async(
2306 file.try_clone()?,
2307 vec![
2308 OwnedWrite {
2309 offset: 32,
2310 bytes: Bytes::from_static(&[0x44; 5]),
2311 },
2312 OwnedWrite {
2313 offset: 48,
2314 bytes: Bytes::from_static(&[0x55; 7]),
2315 },
2316 ],
2317 ))?;
2318 let mut batch = [0_u8; 23];
2319 assert_eq!(read_at(&file, 32, &mut batch)?, 23);
2320 assert_eq!(&batch[..5], &[0x44; 5]);
2321 assert_eq!(&batch[5..16], &[0; 11]);
2322 assert_eq!(&batch[16..], &[0x55; 7]);
2323
2324 let mut cursor = file.try_clone()?;
2327 cursor.seek(SeekFrom::End(0))?;
2328 let selected = complete_read(read_batch_async(
2329 file.try_clone()?,
2330 vec![OwnedRead {
2331 offset: 16,
2332 length: 5,
2333 }],
2334 ))?;
2335 assert_eq!(selected, [Bytes::from_static(&[0x33; 5])]);
2336 assert_eq!(cursor.stream_position()?, 55);
2337
2338 complete_write(write_all_batch_async(
2340 file.try_clone()?,
2341 vec![OwnedWrite {
2342 offset: 4,
2343 bytes: Bytes::from_static(b"cursor-independent"),
2344 }],
2345 ))?;
2346 let mut written = [0_u8; 18];
2347 assert_eq!(read_at(&file, 4, &mut written)?, written.len());
2348 assert_eq!(&written, b"cursor-independent");
2349 assert_eq!(cursor.stream_position()?, 55);
2350 Ok(())
2351 }
2352
2353 #[test]
2354 fn batch_larger_than_native_queue_preserves_every_write() -> io::Result<()> {
2355 let temporary = tempfile::tempdir()?;
2356 let file = OpenOptions::new()
2357 .create_new(true)
2358 .read(true)
2359 .write(true)
2360 .open(temporary.path().join("large-batch"))?;
2361 let writes: Vec<_> = (0_u8..17)
2362 .map(|value| OwnedWrite {
2363 offset: u64::from(value) * 4,
2364 bytes: Bytes::from(vec![value; 3]),
2365 })
2366 .collect();
2367 complete_write(write_all_batch_async(file.try_clone()?, writes))?;
2368
2369 let mut actual = [0_u8; 67];
2370 assert_eq!(read_at(&file, 0, &mut actual)?, actual.len());
2371 for value in 0_u8..17 {
2372 let start = usize::from(value) * 4;
2373 assert_eq!(actual.get(start..start + 3), Some([value; 3].as_slice()));
2374 if value != 16 {
2375 assert_eq!(actual.get(start + 3), Some(&0));
2376 }
2377 }
2378 Ok(())
2379 }
2380
2381 #[test]
2382 fn overlapping_batch_writes_fail_before_any_write() -> io::Result<()> {
2383 let temporary = tempfile::tempdir()?;
2384 let path = temporary.path().join("overlapping-batch");
2385 let file = OpenOptions::new()
2386 .create_new(true)
2387 .read(true)
2388 .write(true)
2389 .open(&path)?;
2390 std::fs::write(&path, b"unchanged")?;
2391 let result = complete_write(write_all_batch_async(
2392 file,
2393 vec![
2394 OwnedWrite {
2395 offset: 0,
2396 bytes: Bytes::from_static(b"first"),
2397 },
2398 OwnedWrite {
2399 offset: 3,
2400 bytes: Bytes::from_static(b"second"),
2401 },
2402 ],
2403 ));
2404 assert!(matches!(result, Err(error) if error.kind() == io::ErrorKind::InvalidInput));
2405 assert_eq!(std::fs::read(&path)?, b"unchanged");
2406 Ok(())
2407 }
2408
2409 #[cfg(target_os = "linux")]
2410 #[test]
2411 fn completed_batches_preserve_ring_reuse() -> io::Result<()> {
2412 let temporary = tempfile::tempdir()?;
2413 let file = OpenOptions::new()
2414 .create_new(true)
2415 .read(true)
2416 .write(true)
2417 .open(temporary.path().join("ring-reuse"))?;
2418 for value in 0_u16..256 {
2419 complete_write(write_all_batch_async(
2420 file.try_clone()?,
2421 vec![OwnedWrite {
2422 offset: u64::from(value),
2423 bytes: Bytes::copy_from_slice(&[value.to_le_bytes()[0]]),
2424 }],
2425 ))?;
2426 }
2427 let contents = complete_read(read_batch_async(
2428 file,
2429 vec![OwnedRead {
2430 offset: 0,
2431 length: 256,
2432 }],
2433 ))?;
2434 assert_eq!(contents.first().map(Bytes::len), Some(256));
2435 Ok(())
2436 }
2437
2438 #[cfg(windows)]
2439 #[test]
2440 fn native_write_batch_preserves_empty_writes_and_port_reuse() -> io::Result<()> {
2441 let temporary = tempfile::tempdir()?;
2442 let file = OpenOptions::new()
2443 .create_new(true)
2444 .read(true)
2445 .write(true)
2446 .open(temporary.path().join("write-batch-reuse"))?;
2447 for value in 0_u8..8 {
2448 complete_write(write_all_batch_async(
2449 file.try_clone()?,
2450 vec![
2451 OwnedWrite {
2452 offset: u64::from(value) * 2,
2453 bytes: Bytes::copy_from_slice(&[value]),
2454 },
2455 OwnedWrite {
2456 offset: u64::from(value) * 2 + 1,
2457 bytes: Bytes::new(),
2458 },
2459 ],
2460 ))?;
2461 }
2462 let mut actual = [0_u8; 15];
2463 assert_eq!(read_at(&file, 0, &mut actual)?, actual.len());
2464 for value in 0_u8..8 {
2465 assert_eq!(actual.get(usize::from(value) * 2), Some(&value));
2466 }
2467 Ok(())
2468 }
2469
2470 #[test]
2471 fn owned_write_outlives_the_callers_file_and_buffers() -> io::Result<()> {
2472 let temporary = tempfile::tempdir()?;
2473 let path = temporary.path().join("owned-write");
2474 let file = OpenOptions::new()
2475 .create_new(true)
2476 .read(true)
2477 .write(true)
2478 .open(&path)?;
2479 let write = write_all_batch_async(
2480 file,
2481 vec![OwnedWrite {
2482 offset: 3,
2483 bytes: Bytes::from_static(b"owned"),
2484 }],
2485 );
2486 complete_write(write)?;
2487 let mut actual = Vec::new();
2488 File::open(path)?.read_to_end(&mut actual)?;
2489 assert_eq!(actual, [0, 0, 0, b'o', b'w', b'n', b'e', b'd']);
2490 Ok(())
2491 }
2492
2493 #[test]
2494 fn owned_native_file_preserves_positional_contract() -> io::Result<()> {
2495 let temporary = tempfile::tempdir()?;
2496 let path = temporary.path().join("owned-native-file");
2497 let file = OpenOptions::new()
2498 .create_new(true)
2499 .read(true)
2500 .write(true)
2501 .open(&path)?;
2502 let native = NativeFile::from_file(file)?;
2503 complete_write(native.set_len_async(32))?;
2504 complete_write(native.write_all_batch_async(vec![OwnedWrite {
2505 offset: 7,
2506 bytes: Bytes::from_static(b"native"),
2507 }]))?;
2508 complete_write(native.write_all_batch_async(vec![
2509 OwnedWrite {
2510 offset: 0,
2511 bytes: Bytes::from_static(b"one"),
2512 },
2513 OwnedWrite {
2514 offset: 20,
2515 bytes: Bytes::from_static(b"two"),
2516 },
2517 ]))?;
2518 complete_write(native.sync_async(Durability::Full))?;
2519
2520 let actual = complete_read(native.read_batch_async(vec![OwnedRead {
2521 offset: 0,
2522 length: 32,
2523 }]))?
2524 .remove(0);
2525 assert_eq!(actual.len(), 32);
2526 assert_eq!(actual.get(..3), Some(b"one".as_slice()));
2527 assert_eq!(actual.get(7..13), Some(b"native".as_slice()));
2528 assert_eq!(actual.get(20..23), Some(b"two".as_slice()));
2529 Ok(())
2530 }
2531
2532 #[test]
2533 fn owned_file_operation_keeps_its_handle_after_owner_drop() -> io::Result<()> {
2534 let temporary = tempfile::tempdir()?;
2535 let path = temporary.path().join("owned-file-lifetime");
2536 let file = OpenOptions::new()
2537 .create_new(true)
2538 .read(true)
2539 .write(true)
2540 .open(&path)?;
2541 let native = NativeFile::from_file(file)?;
2542 let resize = native.set_len_async(64);
2543 drop(native);
2544 complete_write(resize)?;
2545 assert_eq!(std::fs::metadata(&path)?.len(), 64);
2546 Ok(())
2547 }
2548
2549 #[cfg(windows)]
2550 #[test]
2551 fn healthy_registry_does_not_retain_exclusive_handle() -> io::Result<()> {
2552 use std::os::windows::fs::OpenOptionsExt as _;
2553
2554 let temporary = tempfile::tempdir()?;
2555 let path = temporary.path().join("exclusive-native-file");
2556 let file = OpenOptions::new()
2557 .create_new(true)
2558 .read(true)
2559 .write(true)
2560 .share_mode(0)
2561 .open(&path)?;
2562 let native = NativeFile::from_file(file)?;
2563 drop(native);
2564 File::open(&path)?;
2567 Ok(())
2568 }
2569
2570 #[cfg(target_os = "linux")]
2571 #[test]
2572 fn uncertain_completion_retains_identity_after_owner_drop() -> io::Result<()> {
2573 let temporary = tempfile::tempdir()?;
2574 let path = temporary.path().join("uncertain-identity");
2575 let native = NativeFile::from_file(
2576 OpenOptions::new()
2577 .create_new(true)
2578 .read(true)
2579 .write(true)
2580 .open(&path)?,
2581 )?;
2582 let completion = FileCompletion::new(
2583 Arc::new(Mutex::new(CompletionState::<()> {
2584 result: None,
2585 waker: None,
2586 })),
2587 None,
2588 Some(Arc::clone(&native.uncertain)),
2589 Some(Arc::clone(&native.tail)),
2590 )
2591 .for_file(Arc::clone(&native.file));
2592 completion.finish(Err(linux::uncertain_completion(
2593 io::ErrorKind::TimedOut.into(),
2594 )));
2595 drop(native);
2596 let alias = NativeFile::from_file(OpenOptions::new().read(true).open(&path)?)?;
2597 assert!(alias.uncertain.load(Ordering::Acquire));
2598 assert!(
2599 complete_read(alias.read_batch_async(vec![OwnedRead {
2600 offset: 0,
2601 length: 1,
2602 }]))
2603 .is_err()
2604 );
2605 Ok(())
2606 }
2607
2608 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
2609 #[test]
2610 fn concurrent_alias_registration_uses_one_gate() -> io::Result<()> {
2611 let temporary = tempfile::tempdir()?;
2612 let path = temporary.path().join("concurrent-identity");
2613 std::fs::write(&path, b"shared")?;
2614 let start = Arc::new(std::sync::Barrier::new(16));
2615 let mut handles = Vec::new();
2616 for _ in 0..16 {
2617 let path = path.clone();
2618 let start = Arc::clone(&start);
2619 handles.push(std::thread::spawn(move || {
2620 let file = OpenOptions::new().read(true).write(true).open(path)?;
2621 start.wait();
2622 NativeFile::from_file(file)
2623 }));
2624 }
2625 let mut files = Vec::new();
2626 for handle in handles {
2627 files.push(
2628 handle
2629 .join()
2630 .map_err(|_| io::Error::other("registration panicked"))??,
2631 );
2632 }
2633 assert_eq!(files.len(), 16);
2634 if let Some(first) = files.first() {
2635 for file in files.iter().skip(1) {
2636 assert!(Arc::ptr_eq(&first.tail, &file.tail));
2637 assert!(Arc::ptr_eq(&first.uncertain, &file.uncertain));
2638 }
2639 }
2640 Ok(())
2641 }
2642
2643 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
2644 #[test]
2645 fn independently_opened_aliases_share_fencing_and_uncertainty() -> io::Result<()> {
2646 let temporary = tempfile::tempdir()?;
2647 let path = temporary.path().join("identity");
2648 let alias = temporary.path().join("hard-link");
2649 let first = NativeFile::from_file(
2650 OpenOptions::new()
2651 .create_new(true)
2652 .read(true)
2653 .write(true)
2654 .open(&path)?,
2655 )?;
2656 std::fs::hard_link(&path, &alias)?;
2657 let second =
2658 NativeFile::from_file(OpenOptions::new().read(true).write(true).open(&alias)?)?;
2659 assert!(Arc::ptr_eq(&first.tail, &second.tail));
2660 assert!(Arc::ptr_eq(&first.uncertain, &second.uncertain));
2661 poison_file_health(&first.file, &first.uncertain);
2662 drop(first);
2663 let result = complete_write(second.set_len_async(1));
2664 assert!(result.is_err(), "an alias bypassed an uncertain completion");
2665 drop(second);
2666 let bare_result = complete_read(read_batch_async(
2667 OpenOptions::new().read(true).open(&path)?,
2668 vec![OwnedRead {
2669 offset: 0,
2670 length: 1,
2671 }],
2672 ));
2673 assert!(
2674 bare_result.is_err(),
2675 "the free read API bypassed the file fence"
2676 );
2677 let direct_alias = OpenOptions::new().read(true).write(true).open(&alias)?;
2678 let mut byte = [0];
2679 assert!(read_at(&direct_alias, 0, &mut byte).is_err());
2680 assert!(write_all_at(&direct_alias, 0, b"x").is_err());
2681 assert!(sync_file(&direct_alias, Durability::Full).is_err());
2682 assert!(sync_data(&direct_alias, Durability::Full).is_err());
2683 assert_eq!(std::fs::metadata(&path)?.len(), 0);
2684
2685 let independent = NativeFile::from_file(
2686 OpenOptions::new()
2687 .create_new(true)
2688 .read(true)
2689 .write(true)
2690 .open(temporary.path().join("clean"))?,
2691 )?;
2692 complete_write(independent.set_len_async(1))?;
2693 Ok(())
2694 }
2695
2696 #[cfg(any(windows, target_os = "linux", target_vendor = "apple"))]
2697 #[test]
2698 fn admitted_job_keeps_alias_registry_alive_after_owner_drop() -> io::Result<()> {
2699 let temporary = tempfile::tempdir()?;
2700 let path = temporary.path().join("outstanding");
2701 let alias_path = temporary.path().join("outstanding-link");
2702 let native = NativeFile::from_file(
2703 OpenOptions::new()
2704 .create_new(true)
2705 .read(true)
2706 .write(true)
2707 .open(&path)?,
2708 )?;
2709
2710 std::fs::hard_link(&path, &alias_path)?;
2711 let tail = Arc::downgrade(&native.tail);
2712 let (started_sender, started_receiver) = mpsc::sync_channel(1);
2713 let (release_sender, release_receiver) = mpsc::sync_channel::<()>(1);
2714 let mut pending = native.control_async(move |_| {
2715 started_sender.send(()).map_err(io::Error::other)?;
2716 release_receiver.recv().map_err(io::Error::other)?;
2717 Ok(())
2718 });
2719 let waker = completion_waker();
2720 assert!(matches!(
2721 Pin::new(&mut pending).poll(&mut Context::from_waker(&waker)),
2722 Poll::Pending
2723 ));
2724 started_receiver
2725 .recv_timeout(Duration::from_secs(5))
2726 .map_err(io::Error::other)?;
2727 drop(native);
2728 let alias = NativeFile::from_file(
2729 OpenOptions::new()
2730 .read(true)
2731 .write(true)
2732 .open(&alias_path)?,
2733 )?;
2734 assert!(Arc::ptr_eq(
2735 &tail
2736 .upgrade()
2737 .ok_or_else(|| io::Error::other("admitted job lost its alias tail"))?,
2738 &alias.tail
2739 ));
2740 release_sender.send(()).map_err(io::Error::other)?;
2741 complete_write(pending)?;
2742 Ok(())
2743 }
2744
2745 #[test]
2746 fn owned_file_operations_follow_first_poll_order() -> io::Result<()> {
2747 let temporary = tempfile::tempdir()?;
2748 let path = temporary.path().join("owned-file-order");
2749 let native = NativeFile::from_file(
2750 OpenOptions::new()
2751 .create_new(true)
2752 .read(true)
2753 .write(true)
2754 .open(&path)?,
2755 )?;
2756 let write = native.write_all_batch_async(vec![OwnedWrite {
2757 offset: 0,
2758 bytes: Bytes::from_static(b"ordered"),
2759 }]);
2760 let mut sync = native.sync_async(Durability::Full);
2761 let waker = completion_waker();
2762 assert!(matches!(
2763 Pin::new(&mut sync).poll(&mut Context::from_waker(&waker)),
2764 Poll::Pending
2765 ));
2766 complete_write(sync)?;
2767 complete_write(write)?;
2768 assert_eq!(std::fs::read(&path)?, b"ordered");
2769
2770 let discarded = native.write_all_batch_async(vec![OwnedWrite {
2771 offset: 0,
2772 bytes: Bytes::from_static(b"discarded"),
2773 }]);
2774 let resize = native.set_len_async(16);
2775 drop(discarded);
2776 complete_write(resize)?;
2777 assert_eq!(std::fs::metadata(&path)?.len(), 16);
2778 Ok(())
2779 }
2780
2781 #[cfg(target_os = "linux")]
2782 #[test]
2783 fn uncertain_completion_poison_is_file_local_and_fences_successors() -> io::Result<()> {
2784 let temporary = tempfile::tempdir()?;
2785 let poisoned_path = temporary.path().join("poisoned");
2786 let poisoned = NativeFile::from_file(
2787 OpenOptions::new()
2788 .create_new(true)
2789 .read(true)
2790 .write(true)
2791 .open(&poisoned_path)?,
2792 )?;
2793 let prior = Arc::new(OperationFence::default());
2794 *poisoned
2795 .tail
2796 .lock()
2797 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::clone(&prior));
2798 let state = Mutex::new(CompletionState::<()> {
2799 result: None,
2800 waker: None,
2801 });
2802 poison_file_health(&poisoned.file, &poisoned.uncertain);
2803 finish_file_job(
2804 &state,
2805 Err(linux::uncertain_completion(io::Error::other(
2806 "injected ambiguous completion",
2807 ))),
2808 Some(prior),
2809 Some(&poisoned.uncertain),
2810 );
2811 let Err(error) = complete_write(poisoned.set_len_async(4)) else {
2812 return Err(io::Error::other(
2813 "an uncertain prior operation did not fence its successor",
2814 ));
2815 };
2816 assert!(error.to_string().contains("prior native I/O completion"));
2817 assert_eq!(std::fs::metadata(poisoned_path)?.len(), 0);
2818
2819 let unrelated_path = temporary.path().join("unrelated");
2820 let unrelated = NativeFile::from_file(
2821 OpenOptions::new()
2822 .create_new(true)
2823 .read(true)
2824 .write(true)
2825 .open(&unrelated_path)?,
2826 )?;
2827 complete_write(unrelated.set_len_async(4))?;
2828 assert_eq!(std::fs::metadata(unrelated_path)?.len(), 4);
2829 Ok(())
2830 }
2831
2832 #[test]
2833 fn platform_control_is_offloaded_and_file_sequenced() -> io::Result<()> {
2834 let temporary = tempfile::tempdir()?;
2835 let native = NativeFile::from_file(
2836 OpenOptions::new()
2837 .create_new(true)
2838 .read(true)
2839 .write(true)
2840 .open(temporary.path().join("ordered-control"))?,
2841 )?;
2842 let mut write = native.write_all_batch_async(vec![OwnedWrite {
2843 offset: 0,
2844 bytes: Bytes::from_static(b"ready"),
2845 }]);
2846 let waker = completion_waker();
2847 let deadline = Instant::now() + Duration::from_secs(10);
2848 let write_completed = loop {
2849 match Pin::new(&mut write).poll(&mut Context::from_waker(&waker)) {
2850 Poll::Ready(result) => {
2851 result?;
2852 break true;
2853 }
2854 Poll::Pending if write.pending.is_none() => break false,
2855 Poll::Pending if Instant::now() < deadline => {
2856 std::thread::park_timeout(deadline.saturating_duration_since(Instant::now()));
2857 }
2858 Poll::Pending => {
2859 return Err(io::Error::new(
2860 io::ErrorKind::TimedOut,
2861 "write admission stalled",
2862 ));
2863 }
2864 }
2865 };
2866 let (sender, receiver) = mpsc::channel();
2867 let control = native.control_async(move |file| {
2868 let mut bytes = [0; 5];
2869 read_at(file, 0, &mut bytes)?;
2870 sender
2871 .send(bytes)
2872 .map_err(|error| io::Error::other(error.to_string()))
2873 });
2874 complete_write(control)?;
2875 assert_eq!(receiver.recv().map_err(io::Error::other)?, *b"ready");
2876 if !write_completed {
2877 complete_write(write)?;
2878 }
2879 Ok(())
2880 }
2881
2882 #[test]
2883 fn unpolled_operation_does_not_delay_a_later_awaited_operation() -> io::Result<()> {
2884 let temporary = tempfile::tempdir()?;
2885 let path = temporary.path().join("skipped-operation-order");
2886 let native = NativeFile::from_file(
2887 OpenOptions::new()
2888 .create_new(true)
2889 .read(true)
2890 .write(true)
2891 .open(&path)?,
2892 )?;
2893 let never_polled = native.set_len_async(8);
2894 let last = native.set_len_async(32);
2895 complete_write(last)?;
2896 assert_eq!(std::fs::metadata(&path)?.len(), 32);
2897 drop(never_polled);
2898 assert_eq!(std::fs::metadata(&path)?.len(), 32);
2899 Ok(())
2900 }
2901
2902 #[test]
2903 fn skipped_fence_chain_completes_iteratively() -> io::Result<()> {
2904 let first = Arc::new(OperationFence::default());
2905 let mut prior = Arc::clone(&first);
2906 for _ in 0..100_000 {
2907 let skipped = Arc::new(OperationFence::default());
2908 skipped.complete_after(Some(prior));
2909 prior = skipped;
2910 }
2911 first.complete();
2912 assert!(prior.ready_or_register(Waker::noop()));
2913 Ok(())
2914 }
2915
2916 #[test]
2917 fn skipped_fence_does_not_wake_a_dropped_waiter() {
2918 struct CountWake(AtomicU64);
2919 impl Wake for CountWake {
2920 fn wake(self: Arc<Self>) {
2921 self.0.fetch_add(1, Ordering::Relaxed);
2922 }
2923 fn wake_by_ref(self: &Arc<Self>) {
2924 self.0.fetch_add(1, Ordering::Relaxed);
2925 }
2926 }
2927
2928 let predecessor = Arc::new(OperationFence::default());
2929 let skipped = Arc::new(OperationFence::default());
2930 let count = Arc::new(CountWake(AtomicU64::new(0)));
2931 let waker = Waker::from(Arc::clone(&count));
2932 assert!(!predecessor.ready_or_register(&waker));
2933 skipped.complete_after(Some(Arc::clone(&predecessor)));
2934 predecessor.complete();
2935 assert_eq!(count.0.load(Ordering::Relaxed), 0);
2936 assert!(skipped.ready_or_register(Waker::noop()));
2937 }
2938
2939 #[test]
2940 fn saturated_host_transactions_do_not_starve_native_file_io() -> io::Result<()> {
2941 struct GateRelease(Arc<(Mutex<bool>, Condvar)>);
2942 impl Drop for GateRelease {
2943 fn drop(&mut self) {
2944 let (lock, ready) = &*self.0;
2945 *lock
2946 .lock()
2947 .unwrap_or_else(std::sync::PoisonError::into_inner) = true;
2948 ready.notify_all();
2949 }
2950 }
2951
2952 let parallelism = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
2953 let count = parallelism.saturating_mul(2).clamp(2, 16);
2954 let gate = Arc::new((Mutex::new(false), Condvar::new()));
2955 let release = GateRelease(Arc::clone(&gate));
2956 let (entered_tx, entered_rx) = mpsc::channel();
2957 let (finished_tx, finished_rx) = mpsc::channel();
2958 let mut observers = Vec::new();
2959 let waker = completion_waker();
2960 for _ in 0..count {
2961 let gate = Arc::clone(&gate);
2962 let entered_tx = entered_tx.clone();
2963 let finished_tx = finished_tx.clone();
2964 let mut task = run_blocking_io(move || {
2965 let _ = entered_tx.send(());
2966 let (lock, ready) = &*gate;
2967 let mut released = lock
2968 .lock()
2969 .unwrap_or_else(std::sync::PoisonError::into_inner);
2970 while !*released {
2971 released = ready
2972 .wait(released)
2973 .unwrap_or_else(std::sync::PoisonError::into_inner);
2974 }
2975 let _ = finished_tx.send(());
2976 });
2977 assert!(matches!(
2978 Pin::new(&mut task).poll(&mut Context::from_waker(&waker)),
2979 Poll::Pending
2980 ));
2981 observers.push(task);
2982 }
2983 for _ in 0..count {
2984 entered_rx
2985 .recv_timeout(Duration::from_secs(10))
2986 .map_err(|error| io::Error::other(error.to_string()))?;
2987 }
2988 let temporary = tempfile::tempdir()?;
2989 let file = OpenOptions::new()
2990 .create_new(true)
2991 .read(true)
2992 .write(true)
2993 .open(temporary.path().join("independent-native-io"))?;
2994 complete_read(read_batch_async(
2995 file,
2996 vec![OwnedRead {
2997 offset: 0,
2998 length: 1,
2999 }],
3000 ))?;
3001 drop(release);
3002 for _ in 0..count {
3003 finished_rx
3004 .recv_timeout(Duration::from_secs(10))
3005 .map_err(|error| io::Error::other(error.to_string()))?;
3006 }
3007 drop(observers);
3008 Ok(())
3009 }
3010
3011 #[cfg(not(target_vendor = "apple"))]
3012 #[test]
3013 fn barrier_durability_rejects_targets_without_an_exact_primitive() -> io::Result<()> {
3014 let temporary = tempfile::tempdir()?;
3015 let file = File::create(temporary.path().join("barrier"))?;
3016 assert!(matches!(
3017 sync_file(&file, Durability::Barrier),
3018 Err(error) if error.kind() == io::ErrorKind::Unsupported
3019 ));
3020 assert!(matches!(
3021 sync_data(&file, Durability::Barrier),
3022 Err(error) if error.kind() == io::ErrorKind::Unsupported
3023 ));
3024 Ok(())
3025 }
3026
3027 #[test]
3028 fn native_read_batch_preserves_order_and_short_reads() -> io::Result<()> {
3029 let temporary = tempfile::tempdir()?;
3030 let file = OpenOptions::new()
3031 .create_new(true)
3032 .read(true)
3033 .write(true)
3034 .open(temporary.path().join("read-batch"))?;
3035 write_all_at(&file, 0, b"abcdefgh")?;
3036 let reads = complete_read(read_batch_async(
3037 file,
3038 vec![
3039 OwnedRead {
3040 offset: 5,
3041 length: 8,
3042 },
3043 OwnedRead {
3044 offset: 1,
3045 length: 3,
3046 },
3047 ],
3048 ))?;
3049 assert_eq!(
3050 reads,
3051 [Bytes::from_static(b"fgh"), Bytes::from_static(b"bcd")]
3052 );
3053 Ok(())
3054 }
3055
3056 #[cfg(target_vendor = "apple")]
3057 #[test]
3058 fn native_batches_reject_all_invalid_offsets_before_submission() -> io::Result<()> {
3059 let temporary = tempfile::tempdir()?;
3060 let path = temporary.path().join("invalid-offset-batch");
3061 let file = OpenOptions::new()
3062 .create_new(true)
3063 .read(true)
3064 .write(true)
3065 .open(&path)?;
3066 write_all_at(&file, 0, b"stable")?;
3067
3068 let read = complete_read(read_batch_async(
3069 file.try_clone()?,
3070 vec![
3071 OwnedRead {
3072 offset: 0,
3073 length: 1,
3074 },
3075 OwnedRead {
3076 offset: u64::MAX,
3077 length: 1,
3078 },
3079 ],
3080 ));
3081 assert!(matches!(read, Err(error) if error.kind() == io::ErrorKind::InvalidInput));
3082
3083 let write = complete_write(write_all_batch_async(
3084 file,
3085 vec![
3086 OwnedWrite {
3087 offset: 0,
3088 bytes: Bytes::from_static(b"changed"),
3089 },
3090 OwnedWrite {
3091 offset: u64::MAX,
3092 bytes: Bytes::from_static(b"x"),
3093 },
3094 ],
3095 ));
3096 assert!(matches!(write, Err(error) if error.kind() == io::ErrorKind::InvalidInput));
3097 assert_eq!(std::fs::read(path)?, b"stable");
3098 Ok(())
3099 }
3100
3101 #[cfg(target_vendor = "apple")]
3102 #[test]
3103 fn apple_batches_preserve_unlinked_open_files() -> io::Result<()> {
3104 let temporary = tempfile::tempdir()?;
3105 let path = temporary.path().join("unlinked-open-file");
3106 let file = OpenOptions::new()
3107 .create_new(true)
3108 .read(true)
3109 .write(true)
3110 .open(&path)?;
3111 std::fs::remove_file(path)?;
3112 complete_write(write_all_batch_async(
3113 file.try_clone()?,
3114 vec![OwnedWrite {
3115 offset: 3,
3116 bytes: Bytes::from_static(b"kept"),
3117 }],
3118 ))?;
3119 assert_eq!(
3120 complete_read(read_batch_async(
3121 file,
3122 vec![OwnedRead {
3123 offset: 3,
3124 length: 4,
3125 }],
3126 ))?,
3127 [Bytes::from_static(b"kept")]
3128 );
3129 Ok(())
3130 }
3131
3132 #[cfg(target_os = "linux")]
3133 #[test]
3134 fn io_uring_rejects_cursor_aliases_before_any_submission() -> io::Result<()> {
3135 let temporary = tempfile::tempdir()?;
3136 let path = temporary.path().join("invalid-io-uring-offset");
3137 let mut file = OpenOptions::new()
3138 .create_new(true)
3139 .read(true)
3140 .write(true)
3141 .open(&path)?;
3142 write_all_at(&file, 0, b"stable")?;
3143 file.seek(SeekFrom::Start(3))?;
3144
3145 let mut byte = [0_u8; 1];
3146 assert!(matches!(
3147 read_at(&file, u64::MAX, &mut byte),
3148 Err(error) if error.kind() == io::ErrorKind::InvalidInput
3149 ));
3150 assert!(matches!(
3151 write_all_at(&file, u64::MAX, b"x"),
3152 Err(error) if error.kind() == io::ErrorKind::InvalidInput
3153 ));
3154 assert!(matches!(
3155 complete_read(read_batch_async(
3156 file.try_clone()?,
3157 vec![
3158 OwnedRead {
3159 offset: 0,
3160 length: 1,
3161 },
3162 OwnedRead {
3163 offset: u64::MAX,
3164 length: 1,
3165 },
3166 ],
3167 )),
3168 Err(error) if error.kind() == io::ErrorKind::InvalidInput
3169 ));
3170 assert!(matches!(
3171 complete_write(write_all_batch_async(
3172 file.try_clone()?,
3173 vec![
3174 OwnedWrite {
3175 offset: 0,
3176 bytes: Bytes::from_static(b"changed"),
3177 },
3178 OwnedWrite {
3179 offset: u64::MAX,
3180 bytes: Bytes::from_static(b"x"),
3181 },
3182 ],
3183 )),
3184 Err(error) if error.kind() == io::ErrorKind::InvalidInput
3185 ));
3186 assert_eq!(file.stream_position()?, 3);
3187 assert_eq!(std::fs::read(path)?, b"stable");
3188 Ok(())
3189 }
3190
3191 #[test]
3192 fn owned_read_outlives_the_callers_file_and_request() -> io::Result<()> {
3193 let temporary = tempfile::tempdir()?;
3194 let path = temporary.path().join("owned-read");
3195 let file = OpenOptions::new()
3196 .create_new(true)
3197 .read(true)
3198 .write(true)
3199 .open(path)?;
3200 write_all_at(&file, 0, b"abcdefgh")?;
3201 let read = read_batch_async(
3202 file,
3203 vec![OwnedRead {
3204 offset: 2,
3205 length: 4,
3206 }],
3207 );
3208 let actual = complete_read(read)?;
3209 assert_eq!(actual, [Bytes::from_static(b"cdef")]);
3210 Ok(())
3211 }
3212
3213 #[test]
3214 fn admitted_write_outlives_dropped_observer() -> io::Result<()> {
3215 let temporary = tempfile::tempdir()?;
3216 let path = temporary.path().join("detached-write");
3217 let file = OpenOptions::new()
3218 .create_new(true)
3219 .read(true)
3220 .write(true)
3221 .open(&path)?;
3222 let mut write = write_all_batch_async(
3223 file,
3224 vec![OwnedWrite {
3225 offset: 0,
3226 bytes: Bytes::from_static(b"persisted"),
3227 }],
3228 );
3229 let (sender, receiver) = mpsc::sync_channel(1);
3230 let admitted = write
3231 .pending
3232 .take()
3233 .ok_or_else(|| io::Error::other("write was not pending admission"))?;
3234 let completed = Arc::clone(&write.state);
3235 sender
3236 .try_send(admitted)
3237 .map_err(|_| io::Error::other("write admission failed"))?;
3238
3239 drop(write);
3240 let job = receiver
3241 .recv()
3242 .map_err(|_| io::Error::other("admitted write disappeared"))?;
3243 let _ = job.run();
3244
3245 let deadline = Instant::now() + Duration::from_secs(5);
3246 loop {
3247 if let Some(result) = completed
3248 .lock()
3249 .unwrap_or_else(std::sync::PoisonError::into_inner)
3250 .result
3251 .take()
3252 {
3253 result?;
3254 break;
3255 }
3256 if Instant::now() >= deadline {
3257 return Err(io::Error::new(
3258 io::ErrorKind::TimedOut,
3259 "detached admitted write did not complete",
3260 ));
3261 }
3262 std::thread::park_timeout(Duration::from_millis(1));
3263 }
3264
3265 assert_eq!(std::fs::read(path)?, b"persisted");
3266 Ok(())
3267 }
3268
3269 #[test]
3270 fn dropped_admission_waiters_do_not_block_later_reads() -> io::Result<()> {
3271 enum FirstPoll {
3272 Ready(io::Result<Vec<Bytes>>),
3273 Pending(ReadBatch),
3274 }
3275
3276 let temporary = tempfile::tempdir()?;
3277 let path = temporary.path().join("admission-drop");
3278 let file = OpenOptions::new()
3279 .create_new(true)
3280 .read(true)
3281 .write(true)
3282 .open(path)?;
3283 write_all_at(&file, 0, b"x")?;
3284 let mut reads = Vec::new();
3285 for _ in 0..64 {
3286 let mut read = read_batch_async(
3287 file.try_clone()?,
3288 vec![OwnedRead {
3289 offset: 0,
3290 length: 1,
3291 }],
3292 );
3293 reads.push(
3294 match Pin::new(&mut read).poll(&mut Context::from_waker(Waker::noop())) {
3295 Poll::Ready(result) => FirstPoll::Ready(result),
3296 Poll::Pending => FirstPoll::Pending(read),
3297 },
3298 );
3299 }
3300 for read in &reads {
3301 if let FirstPoll::Ready(result) = read {
3302 match result {
3303 Ok(completed) => assert_eq!(completed, &[Bytes::from_static(b"x")]),
3304 Err(error) => {
3305 return Err(io::Error::new(
3306 error.kind(),
3307 format!("initial native read failed: {error}"),
3308 ));
3309 }
3310 }
3311 }
3312 }
3313 reads.truncate(8);
3314 for read in reads {
3315 let completed = match read {
3316 FirstPoll::Ready(result) => result?,
3317 FirstPoll::Pending(read) => complete_read(read)?,
3318 };
3319 assert_eq!(completed, [Bytes::from_static(b"x")]);
3320 }
3321 let final_read = read_batch_async(
3322 file,
3323 vec![OwnedRead {
3324 offset: 0,
3325 length: 1,
3326 }],
3327 );
3328 assert_eq!(complete_read(final_read)?, [Bytes::from_static(b"x")]);
3329 Ok(())
3330 }
3331
3332 #[test]
3333 #[allow(clippy::too_many_lines)]
3334 fn bounded_native_admission_is_work_conserving_and_cancellation_wakes_the_successor()
3335 -> io::Result<()> {
3336 use std::sync::atomic::AtomicUsize;
3337
3338 struct CountWake(AtomicUsize);
3339
3340 impl Wake for CountWake {
3341 fn wake(self: Arc<Self>) {
3342 self.0.fetch_add(1, Ordering::Relaxed);
3343 }
3344
3345 fn wake_by_ref(self: &Arc<Self>) {
3346 self.0.fetch_add(1, Ordering::Relaxed);
3347 }
3348 }
3349
3350 fn job(file: &File) -> io::Result<NativeJob> {
3351 Ok(NativeJob::Read {
3352 file: Arc::new(file.try_clone()?),
3353 reads: vec![OwnedRead {
3354 offset: 0,
3355 length: 1,
3356 }],
3357 #[cfg(windows)]
3358 overlapped: false,
3359 tail: None,
3360 uncertain: None,
3361 state: Arc::new(Mutex::new(CompletionState {
3362 result: None,
3363 waker: None,
3364 })),
3365 completion: None,
3366 })
3367 }
3368
3369 let temporary = tempfile::tempdir()?;
3370 let file = OpenOptions::new()
3371 .create_new(true)
3372 .read(true)
3373 .write(true)
3374 .open(temporary.path().join("fifo-admission"))?;
3375 write_all_at(&file, 0, b"x")?;
3376 let (sender, receiver) = mpsc::sync_channel(1);
3377 let workers = NativeWorkers {
3378 sender,
3379 admission: Arc::new(Mutex::new(AdmissionState::new())),
3380 };
3381 workers
3382 .sender
3383 .try_send(job(&file)?)
3384 .map_err(|_| io::Error::other("failed to saturate test queue"))?;
3385 workers
3386 .admission
3387 .lock()
3388 .map_err(|_| io::Error::other("admission mutex poisoned"))?
3389 .queued_jobs += 1;
3390
3391 let mut first = Some(job(&file)?);
3392 let mut first_waiter = None;
3393 let mut second = Some(job(&file)?);
3394 let mut second_waiter = None;
3395 let noop = Waker::noop();
3396 let context = Context::from_waker(noop);
3397 assert!(matches!(
3398 poll_submission_with(&workers, &mut first, &mut first_waiter, &context),
3399 Poll::Pending
3400 ));
3401 assert!(matches!(
3402 poll_submission_with(&workers, &mut second, &mut second_waiter, &context),
3403 Poll::Pending
3404 ));
3405 let _ = receiver
3406 .recv()
3407 .map_err(|_| io::Error::other("queue closed"))?;
3408 notify_capacity_released(&workers.admission);
3409 assert!(matches!(
3410 poll_submission_with(&workers, &mut second, &mut second_waiter, &context),
3411 Poll::Ready(Ok(()))
3412 ));
3413 assert!(matches!(
3414 poll_submission_with(&workers, &mut first, &mut first_waiter, &context),
3415 Poll::Pending
3416 ));
3417 let _ = receiver
3418 .recv()
3419 .map_err(|_| io::Error::other("queue closed"))?;
3420 notify_capacity_released(&workers.admission);
3421 assert!(matches!(
3422 poll_submission_with(&workers, &mut first, &mut first_waiter, &context),
3423 Poll::Ready(Ok(()))
3424 ));
3425 let _ = receiver
3426 .recv()
3427 .map_err(|_| io::Error::other("queue closed"))?;
3428 notify_capacity_released(&workers.admission);
3429
3430 workers
3431 .sender
3432 .try_send(job(&file)?)
3433 .map_err(|_| io::Error::other("failed to saturate cancellation queue"))?;
3434 workers
3435 .admission
3436 .lock()
3437 .map_err(|_| io::Error::other("admission mutex poisoned"))?
3438 .queued_jobs += 1;
3439 let mut cancelled = Some(job(&file)?);
3440 let mut cancelled_waiter = None;
3441 assert!(matches!(
3442 poll_submission_with(&workers, &mut cancelled, &mut cancelled_waiter, &context),
3443 Poll::Pending
3444 ));
3445 let wake_count = Arc::new(CountWake(AtomicUsize::new(0)));
3446 let successor_waker = Waker::from(Arc::clone(&wake_count));
3447 let successor_context = Context::from_waker(&successor_waker);
3448 let mut successor = Some(job(&file)?);
3449 let mut successor_waiter = None;
3450 assert!(matches!(
3451 poll_submission_with(
3452 &workers,
3453 &mut successor,
3454 &mut successor_waiter,
3455 &successor_context
3456 ),
3457 Poll::Pending
3458 ));
3459 remove_capacity_waiter(&workers, cancelled_waiter.take());
3460 assert_eq!(wake_count.0.load(Ordering::Relaxed), 1);
3461 assert_eq!(
3462 workers
3463 .admission
3464 .lock()
3465 .unwrap_or_else(std::sync::PoisonError::into_inner)
3466 .waiters
3467 .front()
3468 .map(|(identity, _)| *identity),
3469 successor_waiter
3470 );
3471 Ok(())
3472 }
3473
3474 #[test]
3475 fn inactive_admission_waiter_cannot_strand_other_files() -> io::Result<()> {
3476 use std::sync::atomic::AtomicUsize;
3477
3478 struct CountWake(AtomicUsize);
3479 impl Wake for CountWake {
3480 fn wake(self: Arc<Self>) {
3481 self.0.fetch_add(1, Ordering::Relaxed);
3482 }
3483
3484 fn wake_by_ref(self: &Arc<Self>) {
3485 self.0.fetch_add(1, Ordering::Relaxed);
3486 }
3487 }
3488
3489 let (sender, receiver) = mpsc::sync_channel(1);
3490 let workers = NativeWorkers {
3491 sender,
3492 admission: Arc::new(Mutex::new(AdmissionState::new())),
3493 };
3494 let job = || NativeJob::Task(Box::new(|| None));
3495 workers
3496 .sender
3497 .try_send(job())
3498 .map_err(|_| io::Error::other("failed to saturate admission queue"))?;
3499 workers
3500 .admission
3501 .lock()
3502 .map_err(|_| io::Error::other("admission mutex poisoned"))?
3503 .queued_jobs += 1;
3504
3505 let mut inactive = Some(job());
3506 let mut inactive_waiter = None;
3507 assert!(matches!(
3508 poll_submission_with(
3509 &workers,
3510 &mut inactive,
3511 &mut inactive_waiter,
3512 &Context::from_waker(Waker::noop()),
3513 ),
3514 Poll::Pending
3515 ));
3516 let wakes = Arc::new(CountWake(AtomicUsize::new(0)));
3517 let active_waker = Waker::from(Arc::clone(&wakes));
3518 let context = Context::from_waker(&active_waker);
3519 let mut active = Some(job());
3520 let mut active_waiter = None;
3521 assert!(matches!(
3522 poll_submission_with(&workers, &mut active, &mut active_waiter, &context),
3523 Poll::Pending
3524 ));
3525
3526 let _ = receiver.recv().map_err(io::Error::other)?;
3527 notify_capacity_released(&workers.admission);
3528 assert!(wakes.0.load(Ordering::Relaxed) > 0);
3529 assert!(matches!(
3530 poll_submission_with(&workers, &mut active, &mut active_waiter, &context),
3531 Poll::Ready(Ok(()))
3532 ));
3533 let _ = receiver.recv().map_err(io::Error::other)?;
3534 notify_capacity_released(&workers.admission);
3535 assert!(matches!(
3536 poll_submission_with(
3537 &workers,
3538 &mut inactive,
3539 &mut inactive_waiter,
3540 &Context::from_waker(Waker::noop()),
3541 ),
3542 Poll::Ready(Ok(()))
3543 ));
3544 let _ = receiver.recv().map_err(io::Error::other)?;
3545 notify_capacity_released(&workers.admission);
3546 assert_eq!(
3547 workers
3548 .admission
3549 .lock()
3550 .map_err(|_| io::Error::other("admission mutex poisoned"))?
3551 .queued_jobs,
3552 0
3553 );
3554 Ok(())
3555 }
3556
3557 #[cfg(windows)]
3558 #[test]
3559 fn native_read_batch_preserves_empty_and_eof_ranges() -> io::Result<()> {
3560 let temporary = tempfile::tempdir()?;
3561 let file = OpenOptions::new()
3562 .create_new(true)
3563 .read(true)
3564 .write(true)
3565 .open(temporary.path().join("read-batch-eof"))?;
3566 write_all_at(&file, 0, b"abc")?;
3567 let reads = complete_read(read_batch_async(
3568 file.try_clone()?,
3569 vec![
3570 OwnedRead {
3571 offset: 0,
3572 length: 0,
3573 },
3574 OwnedRead {
3575 offset: 3,
3576 length: 4,
3577 },
3578 OwnedRead {
3579 offset: 30,
3580 length: 4,
3581 },
3582 ],
3583 ))?;
3584 assert_eq!(reads, [Bytes::new(), Bytes::new(), Bytes::new()]);
3585 for (offset, expected) in b"abc".iter().copied().enumerate() {
3586 let repeated = complete_read(read_batch_async(
3587 file.try_clone()?,
3588 vec![OwnedRead {
3589 offset: offset as u64,
3590 length: 1,
3591 }],
3592 ))?;
3593 assert_eq!(repeated, [Bytes::copy_from_slice(&[expected])]);
3594 }
3595 Ok(())
3596 }
3597
3598 #[test]
3599 #[ignore = "local backend comparison; run with --ignored --nocapture on each target"]
3600 fn owned_io_backend_baseline() -> io::Result<()> {
3601 const BLOCK_BYTES: usize = 64 * 1024;
3602 const BLOCKS: usize = 256;
3603 let directory = tempfile::tempdir()?;
3604 let payload = Bytes::from(vec![0x5a; BLOCK_BYTES]);
3605 let baseline = OpenOptions::new()
3606 .create_new(true)
3607 .read(true)
3608 .write(true)
3609 .open(directory.path().join("baseline"))?;
3610 let native = NativeFile::from_file(
3611 OpenOptions::new()
3612 .create_new(true)
3613 .read(true)
3614 .write(true)
3615 .open(directory.path().join("native"))?,
3616 )?;
3617
3618 let copy_start = Instant::now();
3619 for _ in 0..BLOCKS {
3620 std::hint::black_box(Bytes::copy_from_slice(&payload));
3621 }
3622 let copy_elapsed = copy_start.elapsed();
3623
3624 let baseline_start = Instant::now();
3625 for block in 0..BLOCKS {
3626 direct_write_at(&baseline, &payload, (block * BLOCK_BYTES) as u64)?;
3627 }
3628 let baseline_write = baseline_start.elapsed();
3629 baseline.sync_all()?;
3630 let baseline_sync = baseline_start.elapsed();
3631 let mut buffer = vec![0_u8; BLOCK_BYTES];
3632 for block in 0..BLOCKS {
3633 assert_eq!(
3634 direct_read_at(&baseline, &mut buffer, (block * BLOCK_BYTES) as u64)?,
3635 BLOCK_BYTES
3636 );
3637 assert_eq!(buffer, payload);
3638 }
3639 let baseline_elapsed = baseline_start.elapsed();
3640
3641 complete_read(native.read_batch_async(Vec::new()))?;
3642 let native_start = Instant::now();
3643 complete_write(
3644 native.write_all_batch_async(
3645 (0..BLOCKS)
3646 .map(|block| OwnedWrite {
3647 offset: (block * BLOCK_BYTES) as u64,
3648 bytes: payload.clone(),
3649 })
3650 .collect(),
3651 ),
3652 )?;
3653 let native_write = native_start.elapsed();
3654 complete_write(native.sync_async(Durability::Full))?;
3655 let native_sync = native_start.elapsed();
3656 let reads = complete_read(
3657 native.read_batch_async(
3658 (0..BLOCKS)
3659 .map(|block| OwnedRead {
3660 offset: (block * BLOCK_BYTES) as u64,
3661 length: BLOCK_BYTES,
3662 })
3663 .collect(),
3664 ),
3665 )?;
3666 assert_eq!(reads.len(), BLOCKS);
3667 assert!(reads.iter().all(|bytes| bytes == &payload));
3668 let native_elapsed = native_start.elapsed();
3669 let serial_start = Instant::now();
3670 for block in 0..BLOCKS {
3671 let read = complete_read(native.read_batch_async(vec![OwnedRead {
3672 offset: (block * BLOCK_BYTES) as u64,
3673 length: BLOCK_BYTES,
3674 }]))?;
3675 assert_eq!(read.as_slice(), std::slice::from_ref(&payload));
3676 }
3677 let native_serial_read = serial_start.elapsed();
3678 eprintln!(
3679 "owned-io-baseline os={} arch={} bytes={} copy_us={} baseline_us={}/{}/{} native_us={}/{}/{} native_serial_read_us={}",
3680 std::env::consts::OS,
3681 std::env::consts::ARCH,
3682 BLOCKS * BLOCK_BYTES * 2,
3683 copy_elapsed.as_micros(),
3684 baseline_write.as_micros(),
3685 baseline_sync.as_micros() - baseline_write.as_micros(),
3686 baseline_elapsed.as_micros() - baseline_sync.as_micros(),
3687 native_write.as_micros(),
3688 native_sync.as_micros() - native_write.as_micros(),
3689 native_elapsed.as_micros() - native_sync.as_micros(),
3690 native_serial_read.as_micros(),
3691 );
3692 Ok(())
3693 }
3694
3695 #[cfg(target_os = "linux")]
3696 #[test]
3697 fn native_read_batch_chunks_large_ranges() -> io::Result<()> {
3698 let temporary = tempfile::tempdir()?;
3699 let file = OpenOptions::new()
3700 .create_new(true)
3701 .read(true)
3702 .write(true)
3703 .open(temporary.path().join("large-read-batch"))?;
3704 let expected = Bytes::from(vec![0x5a; 1024 * 1024 + 1]);
3705 write_all_at(&file, 7, &expected)?;
3706 let actual = complete_read(read_batch_async(
3707 file,
3708 vec![OwnedRead {
3709 offset: 7,
3710 length: expected.len(),
3711 }],
3712 ))?;
3713 assert_eq!(actual, [expected]);
3714 Ok(())
3715 }
3716
3717 #[cfg(target_os = "linux")]
3718 #[test]
3719 fn native_read_batch_windows_requests_beyond_the_ring() -> io::Result<()> {
3720 let temporary = tempfile::tempdir()?;
3721 let file = OpenOptions::new()
3722 .create_new(true)
3723 .read(true)
3724 .write(true)
3725 .open(temporary.path().join("windowed-read-batch"))?;
3726 let expected: Vec<u8> = (0..17).collect();
3727 write_all_at(&file, 0, &expected)?;
3728 let requests = (0_u64..17)
3729 .map(|offset| OwnedRead { offset, length: 1 })
3730 .collect();
3731 let actual = complete_read(read_batch_async(file, requests))?;
3732 assert_eq!(
3733 actual,
3734 expected
3735 .into_iter()
3736 .map(|value| Bytes::copy_from_slice(&[value]))
3737 .collect::<Vec<_>>()
3738 );
3739 Ok(())
3740 }
3741
3742 #[cfg(target_os = "linux")]
3743 #[test]
3744 fn native_read_batch_windows_one_range_beyond_the_ring() -> io::Result<()> {
3745 let temporary = tempfile::tempdir()?;
3746 let file = OpenOptions::new()
3747 .create_new(true)
3748 .read(true)
3749 .write(true)
3750 .open(temporary.path().join("windowed-large-read"))?;
3751 let expected = Bytes::from(vec![0x69; 16 * 1024 * 1024 + 1]);
3752 write_all_at(&file, 3, &expected)?;
3753 let actual = complete_read(read_batch_async(
3754 file,
3755 vec![OwnedRead {
3756 offset: 3,
3757 length: expected.len(),
3758 }],
3759 ))?;
3760 assert_eq!(actual, [expected]);
3761 Ok(())
3762 }
3763}