1use bytes::Bytes;
2#[cfg(target_os = "linux")]
3use io_uring::{opcode, types, IoUring, Probe};
4#[cfg(any(target_os = "linux", test))]
5use std::collections::HashMap;
6use std::fs::File;
7#[cfg(any(unix, target_os = "windows", test))]
8use std::io;
9#[cfg(unix)]
10use std::os::unix::io::RawFd;
11use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
12use std::sync::Arc;
13#[cfg(any(target_os = "linux", test))]
14use std::sync::{Mutex, OnceLock};
15
16use crate::constants::*;
17use crate::error::{FeoxError, Result};
18use crate::storage::allocation_journal::{
19 decode as decode_allocation_journal, encode_active as encode_active_allocation_journal,
20 encode_clear as encode_clear_allocation_journal, ALLOCATION_JOURNAL_BLOCKS,
21 ALLOCATION_JOURNAL_MAX_ENTRIES, ALLOCATION_JOURNAL_SLOTS, ALLOCATION_JOURNAL_SLOT_BLOCKS,
22 ALLOCATION_JOURNAL_START_BLOCK,
23};
24use crate::storage::format::fill_retirement_markers;
25use crate::storage::metadata::Metadata;
26#[cfg(unix)]
27use crate::utils::allocator::AlignedBuffer;
28
29#[cfg(target_os = "linux")]
30enum PendingWriteBuffer {
31 Aligned(AlignedBuffer),
32 Shared(Bytes),
33}
34
35#[cfg(target_os = "linux")]
36impl PendingWriteBuffer {
37 fn as_ptr(&self) -> *const u8 {
38 match self {
39 Self::Aligned(buffer) => buffer.as_ptr(),
40 Self::Shared(buffer) => buffer.as_ptr(),
41 }
42 }
43
44 fn len(&self) -> usize {
45 match self {
46 Self::Aligned(buffer) => buffer.len(),
47 Self::Shared(buffer) => buffer.len(),
48 }
49 }
50}
51
52trait BatchWriteData {
53 fn as_slice(&self) -> &[u8];
54
55 #[cfg(target_os = "linux")]
56 fn retain_for_write(&self) -> Bytes;
57}
58
59impl BatchWriteData for Vec<u8> {
60 fn as_slice(&self) -> &[u8] {
61 self
62 }
63
64 #[cfg(target_os = "linux")]
65 fn retain_for_write(&self) -> Bytes {
66 Bytes::copy_from_slice(self)
67 }
68}
69
70impl BatchWriteData for Bytes {
71 fn as_slice(&self) -> &[u8] {
72 self
73 }
74
75 #[cfg(target_os = "linux")]
76 fn retain_for_write(&self) -> Bytes {
77 self.clone()
78 }
79}
80
81#[cfg(any(target_os = "linux", test))]
82#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
83struct FileIdentity {
84 device: u64,
85 inode: u64,
86}
87
88#[cfg(any(target_os = "linux", test))]
90static INDETERMINATE_FILES: OnceLock<Mutex<HashMap<FileIdentity, Arc<File>>>> = OnceLock::new();
91
92#[cfg(any(target_os = "linux", test))]
93fn indeterminate_files() -> &'static Mutex<HashMap<FileIdentity, Arc<File>>> {
94 INDETERMINATE_FILES.get_or_init(|| Mutex::new(HashMap::new()))
95}
96
97#[cfg(any(target_os = "linux", test))]
98fn file_is_indeterminate(identity: FileIdentity) -> bool {
99 indeterminate_files()
100 .lock()
101 .unwrap_or_else(|poisoned| poisoned.into_inner())
102 .contains_key(&identity)
103}
104
105#[cfg(any(target_os = "linux", test))]
106fn mark_file_indeterminate(identity: FileIdentity, file: &Arc<File>) {
107 indeterminate_files()
108 .lock()
109 .unwrap_or_else(|poisoned| poisoned.into_inner())
110 .entry(identity)
111 .or_insert_with(|| Arc::clone(file));
112}
113
114#[cfg(target_os = "linux")]
115fn file_identity(file: &File) -> Result<FileIdentity> {
116 use std::os::unix::fs::MetadataExt;
117
118 let metadata = file.metadata().map_err(FeoxError::IoError)?;
119 Ok(FileIdentity {
120 device: metadata.dev(),
121 inode: metadata.ino(),
122 })
123}
124
125#[cfg(any(target_os = "linux", test))]
126struct InFlightBuffers<T> {
127 buffers: Vec<Option<T>>,
128 in_flight: u128,
129}
130
131#[cfg(any(target_os = "linux", test))]
132impl<T> InFlightBuffers<T> {
133 fn with_capacity(capacity: usize) -> Self {
134 assert!(capacity <= u128::BITS as usize);
135 Self {
136 buffers: Vec::with_capacity(capacity),
137 in_flight: 0,
138 }
139 }
140
141 fn push(&mut self, buffer: T) {
142 assert!(self.buffers.len() < u128::BITS as usize);
143 self.buffers.push(Some(buffer));
144 }
145
146 #[cfg(test)]
147 fn new(buffers: Vec<T>) -> Self {
148 let mut in_flight = Self::with_capacity(buffers.len());
149 for buffer in buffers {
150 in_flight.push(buffer);
151 }
152 in_flight
153 }
154
155 fn get(&self, index: usize) -> &T {
156 self.buffers[index]
157 .as_ref()
158 .expect("in-flight buffer is owned")
159 }
160
161 fn mark_in_flight(&mut self, index: usize) {
162 self.in_flight |= 1 << index;
163 }
164
165 fn mark_unqueued(&mut self, index: usize) {
166 self.in_flight &= !(1 << index);
167 }
168
169 fn mark_complete(&mut self, index: usize) -> bool {
170 let mask = 1 << index;
171 let was_in_flight = self.in_flight & mask != 0;
172 self.in_flight &= !mask;
173 was_in_flight
174 }
175}
176
177#[cfg(any(target_os = "linux", test))]
178impl<T> Drop for InFlightBuffers<T> {
179 fn drop(&mut self) {
180 for (index, buffer) in self.buffers.iter_mut().enumerate() {
181 if self.in_flight & (1 << index) != 0 {
182 std::mem::forget(buffer.take());
184 }
185 }
186 }
187}
188
189#[cfg(any(target_os = "linux", test))]
190fn validate_write_completion(result: i32, expected: usize) -> io::Result<()> {
191 if result < 0 {
192 return Err(io::Error::from_raw_os_error(-result));
193 }
194 if result as usize != expected {
195 return Err(io::Error::new(
196 io::ErrorKind::WriteZero,
197 format!("Wrote {result} bytes, expected {expected}"),
198 ));
199 }
200 Ok(())
201}
202
203fn coalesce_extents(extents: &[(u64, usize)]) -> Result<Vec<(u64, usize)>> {
204 let mut ordered = extents.to_vec();
205 ordered.sort_unstable_by_key(|extent| extent.0);
206
207 let mut coalesced: Vec<(u64, usize)> = Vec::with_capacity(ordered.len());
208 for (sector, sectors) in ordered {
209 if sectors == 0 {
210 return Err(FeoxError::InvalidArgument);
211 }
212 let end = sector
213 .checked_add(sectors as u64)
214 .ok_or(FeoxError::InvalidArgument)?;
215
216 let Some(previous) = coalesced.last_mut() else {
217 coalesced.push((sector, sectors));
218 continue;
219 };
220 let previous_end = previous
221 .0
222 .checked_add(previous.1 as u64)
223 .ok_or(FeoxError::InvalidArgument)?;
224 if sector < previous_end {
225 return Err(FeoxError::InvalidArgument);
226 }
227 if sector == previous_end {
228 previous.1 =
229 usize::try_from(end - previous.0).map_err(|_| FeoxError::InvalidArgument)?;
230 } else {
231 coalesced.push((sector, sectors));
232 }
233 }
234 Ok(coalesced)
235}
236
237const RETIREMENT_WRITE_BLOCKS: usize = 256;
238
239pub struct DiskIO {
240 #[cfg(target_os = "linux")]
241 ring: Option<IoUring>,
242 #[cfg(target_os = "linux")]
243 next_user_data: u64,
244 write_indeterminate: AtomicBool,
245 journal_generation: AtomicU64,
246 journal_slot: AtomicUsize,
247 #[cfg(target_os = "linux")]
248 file_identity: FileIdentity,
249 _file: Arc<File>,
250 #[cfg(unix)]
251 fd: RawFd,
252 _use_direct_io: bool,
253}
254
255impl DiskIO {
256 #[cfg(unix)]
257 pub fn new(file: Arc<File>, use_direct_io: bool) -> Result<Self> {
258 use std::os::unix::io::AsRawFd;
259 let fd = file.as_raw_fd();
260 #[cfg(target_os = "linux")]
261 {
262 let file_identity = file_identity(file.as_ref())?;
263 if file_is_indeterminate(file_identity) {
264 return Err(FeoxError::IndeterminateWrite(io::Error::other(
265 "io_uring write outcome for this file is indeterminate until process restart",
266 )));
267 }
268
269 let ring: Option<IoUring> = IoUring::builder()
271 .setup_sqpoll(IOURING_SQPOLL_IDLE_MS)
272 .build(IOURING_QUEUE_SIZE)
273 .ok();
274
275 if let Some(ref r) = ring {
276 let mut probe = Probe::new();
277 if r.submitter().register_probe(&mut probe).is_ok()
278 && probe.is_supported(opcode::Read::CODE)
279 && probe.is_supported(opcode::Write::CODE)
280 {
281 return Ok(Self {
282 ring,
283 next_user_data: 0,
284 write_indeterminate: AtomicBool::new(false),
285 journal_generation: AtomicU64::new(0),
286 journal_slot: AtomicUsize::new(ALLOCATION_JOURNAL_SLOTS - 1),
287 file_identity,
288 _file: file.clone(),
289 fd,
290 _use_direct_io: use_direct_io,
291 });
292 }
293 }
294
295 Ok(Self {
296 ring: None,
297 next_user_data: 0,
298 write_indeterminate: AtomicBool::new(false),
299 journal_generation: AtomicU64::new(0),
300 journal_slot: AtomicUsize::new(ALLOCATION_JOURNAL_SLOTS - 1),
301 file_identity,
302 _file: file,
303 fd,
304 _use_direct_io: use_direct_io,
305 })
306 }
307
308 #[cfg(not(target_os = "linux"))]
309 {
310 let _ = use_direct_io; Ok(Self {
312 write_indeterminate: AtomicBool::new(false),
313 journal_generation: AtomicU64::new(0),
314 journal_slot: AtomicUsize::new(ALLOCATION_JOURNAL_SLOTS - 1),
315 _file: file,
316 fd,
317 _use_direct_io: false, })
319 }
320 }
321
322 #[cfg(not(unix))]
323 pub fn new_from_file(file: File) -> Result<Self> {
324 Ok(Self {
325 write_indeterminate: AtomicBool::new(false),
326 journal_generation: AtomicU64::new(0),
327 journal_slot: AtomicUsize::new(ALLOCATION_JOURNAL_SLOTS - 1),
328 _file: Arc::new(file),
329 _use_direct_io: false,
330 })
331 }
332
333 pub fn read_sectors_sync(&self, sector: u64, count: u64) -> Result<Vec<u8>> {
334 let size = (count * FEOX_BLOCK_SIZE as u64) as usize;
335 let offset = sector * FEOX_BLOCK_SIZE as u64;
336
337 #[cfg(unix)]
338 {
339 if self._use_direct_io {
341 let mut buffer = AlignedBuffer::new(size)?;
342 buffer.set_len(size);
343
344 let read = unsafe {
345 libc::pread(
346 self.fd,
347 buffer.as_mut_ptr() as *mut libc::c_void,
348 size,
349 offset as libc::off_t,
350 )
351 };
352
353 if read < 0 {
354 let err = io::Error::last_os_error();
355 return Err(FeoxError::IoError(err));
356 }
357
358 if read as usize != size {
359 return Err(FeoxError::IoError(io::Error::new(
360 io::ErrorKind::UnexpectedEof,
361 format!("Read {} bytes, expected {}", read, size),
362 )));
363 }
364
365 Ok(buffer.as_slice().to_vec())
367 } else {
368 let mut buffer = vec![0u8; size];
370
371 let read = unsafe {
372 libc::pread(
373 self.fd,
374 buffer.as_mut_ptr() as *mut libc::c_void,
375 size,
376 offset as libc::off_t,
377 )
378 };
379
380 if read < 0 {
381 let err = io::Error::last_os_error();
382 return Err(FeoxError::IoError(err));
383 }
384
385 if read as usize != size {
386 return Err(FeoxError::IoError(io::Error::new(
387 io::ErrorKind::UnexpectedEof,
388 format!("Read {} bytes, expected {}", read, size),
389 )));
390 }
391
392 buffer.truncate(read as usize);
393 Ok(buffer)
394 }
395 }
396
397 #[cfg(not(unix))]
398 {
399 let mut buffer = vec![0u8; size];
401
402 #[cfg(target_os = "windows")]
404 {
405 use std::os::windows::fs::FileExt;
406 let read = self
407 ._file
408 .seek_read(&mut buffer, offset)
409 .map_err(FeoxError::IoError)?;
410 if read != size {
411 return Err(FeoxError::IoError(io::Error::new(
412 io::ErrorKind::UnexpectedEof,
413 format!("Read {read} bytes, expected {size}"),
414 )));
415 }
416 }
417
418 #[cfg(not(any(unix, target_os = "windows")))]
419 {
420 use std::io::{Read, Seek, SeekFrom};
422
423 let mut file = self
425 ._file
426 .as_ref()
427 .try_clone()
428 .map_err(FeoxError::IoError)?;
429
430 file.seek(SeekFrom::Start(offset))
431 .map_err(FeoxError::IoError)?;
432
433 file.read_exact(&mut buffer).map_err(FeoxError::IoError)?;
434 }
435
436 Ok(buffer)
437 }
438 }
439
440 pub fn write_sectors_sync(&self, sector: u64, data: &[u8]) -> Result<()> {
441 self.ensure_writable()?;
442 let offset = sector * FEOX_BLOCK_SIZE as u64;
443
444 #[cfg(unix)]
445 {
446 let written = if self._use_direct_io {
447 let mut aligned_buffer = AlignedBuffer::new(data.len())?;
449 aligned_buffer.set_len(data.len());
450 aligned_buffer.as_mut_slice().copy_from_slice(data);
451
452 unsafe {
453 libc::pwrite(
454 self.fd,
455 aligned_buffer.as_ptr() as *const libc::c_void,
456 aligned_buffer.len(),
457 offset as libc::off_t,
458 )
459 }
460 } else {
461 unsafe {
463 libc::pwrite(
464 self.fd,
465 data.as_ptr() as *const libc::c_void,
466 data.len(),
467 offset as libc::off_t,
468 )
469 }
470 };
471
472 if written < 0 {
473 return Err(FeoxError::IoError(io::Error::last_os_error()));
474 }
475
476 if written as usize != data.len() {
477 return Err(FeoxError::IoError(io::Error::new(
478 io::ErrorKind::UnexpectedEof,
479 "Partial write",
480 )));
481 }
482 }
483
484 #[cfg(not(unix))]
485 {
486 #[cfg(target_os = "windows")]
487 {
488 use std::os::windows::fs::FileExt;
489 let written = self
490 ._file
491 .seek_write(data, offset)
492 .map_err(FeoxError::IoError)?;
493 if written != data.len() {
494 return Err(FeoxError::IoError(io::Error::new(
495 io::ErrorKind::WriteZero,
496 format!("Wrote {written} bytes, expected {}", data.len()),
497 )));
498 }
499 }
500
501 #[cfg(not(any(unix, target_os = "windows")))]
502 {
503 use std::io::{Seek, SeekFrom, Write};
505
506 let mut file = self
508 ._file
509 .as_ref()
510 .try_clone()
511 .map_err(FeoxError::IoError)?;
512
513 file.seek(SeekFrom::Start(offset))
514 .map_err(FeoxError::IoError)?;
515
516 file.write_all(data).map_err(FeoxError::IoError)?;
517
518 file.sync_data().map_err(FeoxError::IoError)?;
520 }
521 }
522
523 Ok(())
524 }
525
526 pub fn flush(&self) -> Result<()> {
527 self.ensure_writable()?;
528 #[cfg(unix)]
529 unsafe {
530 if libc::fsync(self.fd) == -1 {
531 return Err(FeoxError::IoError(io::Error::last_os_error()));
532 }
533 }
534
535 #[cfg(not(unix))]
536 {
537 self._file.sync_all().map_err(FeoxError::IoError)?;
538 }
539
540 Ok(())
541 }
542
543 pub(crate) fn read_allocation_journal(&self, total_sectors: u64) -> Result<Vec<(u64, usize)>> {
544 let data =
545 self.read_sectors_sync(ALLOCATION_JOURNAL_START_BLOCK, ALLOCATION_JOURNAL_BLOCKS)?;
546 let state = decode_allocation_journal(&data, total_sectors)?;
547 self.journal_generation
548 .store(state.generation, Ordering::Release);
549 self.journal_slot.store(state.slot, Ordering::Release);
550 Ok(state.extents)
551 }
552
553 pub(crate) fn write_allocation_journal(&self, extents: &[(u64, usize)]) -> Result<()> {
554 let (generation, slot) = self.next_journal_position()?;
555 let journal = encode_active_allocation_journal(generation, extents)?;
556 self.write_sectors_sync(self.journal_sector(slot), &journal)?;
557 self.flush()?;
558 self.journal_generation.store(generation, Ordering::Release);
559 self.journal_slot.store(slot, Ordering::Release);
560 Ok(())
561 }
562
563 pub(crate) fn clear_allocation_journal(&self) -> Result<()> {
564 let (generation, slot) = self.next_journal_position()?;
565 let journal = encode_clear_allocation_journal(generation)?;
566 self.write_sectors_sync(self.journal_sector(slot), &journal)?;
567 self.flush()?;
568 self.journal_generation.store(generation, Ordering::Release);
569 self.journal_slot.store(slot, Ordering::Release);
570 Ok(())
571 }
572
573 pub(crate) fn poison_writes(&self, error: FeoxError) -> FeoxError {
574 self.write_indeterminate.store(true, Ordering::Release);
575 #[cfg(target_os = "linux")]
576 mark_file_indeterminate(self.file_identity, &self._file);
577 FeoxError::IndeterminateWrite(std::io::Error::other(error.to_string()))
578 }
579
580 fn ensure_writable(&self) -> Result<()> {
581 if self.write_indeterminate.load(Ordering::Acquire) {
582 return Err(FeoxError::IndeterminateWrite(std::io::Error::other(
583 "write outcome is indeterminate until restart",
584 )));
585 }
586 Ok(())
587 }
588
589 fn next_journal_position(&self) -> Result<(u64, usize)> {
590 let generation = self
591 .journal_generation
592 .load(Ordering::Acquire)
593 .checked_add(1)
594 .ok_or(FeoxError::InvalidMetadata)?;
595 let slot = (self.journal_slot.load(Ordering::Acquire) + 1) % ALLOCATION_JOURNAL_SLOTS;
596 Ok((generation, slot))
597 }
598
599 fn journal_sector(&self, slot: usize) -> u64 {
600 ALLOCATION_JOURNAL_START_BLOCK + slot as u64 * ALLOCATION_JOURNAL_SLOT_BLOCKS
601 }
602
603 pub(crate) fn retire_extents(&self, extents: &[(u64, usize)]) -> Result<()> {
604 if extents.is_empty() {
605 return Ok(());
606 }
607
608 let coalesced = coalesce_extents(extents)?;
609 for chunk in coalesced.chunks(ALLOCATION_JOURNAL_MAX_ENTRIES) {
610 if let Err(error) = self.write_allocation_journal(chunk) {
611 return Err(self.poison_writes(error));
612 }
613 if let Err(error) = self.retire_extents_unjournaled(chunk) {
614 return Err(self.poison_writes(error));
615 }
616 if let Err(error) = self.clear_allocation_journal() {
617 return Err(self.poison_writes(error));
618 }
619 }
620
621 Ok(())
622 }
623
624 pub(crate) fn replay_allocation_journal(&self, extents: &[(u64, usize)]) -> Result<()> {
625 if extents.is_empty() {
626 return Ok(());
627 }
628
629 let coalesced = coalesce_extents(extents)?;
630 self.retire_extents_unjournaled(&coalesced)?;
631 self.clear_allocation_journal()
632 }
633
634 fn retire_extents_unjournaled(&self, extents: &[(u64, usize)]) -> Result<()> {
635 if extents.iter().any(|(_, sectors)| *sectors == 0) {
636 return Err(FeoxError::InvalidArgument);
637 }
638
639 let scratch_blocks = extents
640 .iter()
641 .map(|(_, sectors)| (*sectors).min(RETIREMENT_WRITE_BLOCKS))
642 .max()
643 .ok_or(FeoxError::InvalidArgument)?;
644 let scratch_size = scratch_blocks
645 .checked_mul(FEOX_BLOCK_SIZE)
646 .ok_or(FeoxError::InvalidArgument)?;
647
648 #[cfg(unix)]
649 if self._use_direct_io {
650 self.ensure_writable()?;
651 let mut scratch = AlignedBuffer::new(scratch_size)?;
652 scratch.set_len(scratch_size);
653 scratch.as_mut_slice().fill(0);
654 for &(sector, sectors) in extents {
655 self.write_retirement_extent_direct(sector, sectors, &mut scratch)?;
656 }
657 return self.flush();
658 }
659
660 let mut scratch = vec![0; scratch_size];
661 for &(sector, sectors) in extents {
662 self.write_retirement_extent_buffered(sector, sectors, &mut scratch)?;
663 }
664 self.flush()
665 }
666
667 fn write_retirement_extent_buffered(
668 &self,
669 sector: u64,
670 sectors: usize,
671 scratch: &mut [u8],
672 ) -> Result<()> {
673 let mut offset = 0;
674 while offset < sectors {
675 let blocks = (sectors - offset).min(RETIREMENT_WRITE_BLOCKS);
676 let size = blocks
677 .checked_mul(FEOX_BLOCK_SIZE)
678 .ok_or(FeoxError::InvalidArgument)?;
679 let block_sector = sector + offset as u64;
680 let remaining = sectors - offset;
681 let retired = &mut scratch[..size];
682 fill_retirement_markers(retired, block_sector, remaining);
683 self.write_sectors_sync(block_sector, retired)?;
684
685 offset += blocks;
686 }
687
688 Ok(())
689 }
690
691 #[cfg(unix)]
692 fn write_retirement_extent_direct(
693 &self,
694 sector: u64,
695 sectors: usize,
696 scratch: &mut AlignedBuffer,
697 ) -> Result<()> {
698 let mut offset = 0;
699 while offset < sectors {
700 let blocks = (sectors - offset).min(RETIREMENT_WRITE_BLOCKS);
701 let size = blocks
702 .checked_mul(FEOX_BLOCK_SIZE)
703 .ok_or(FeoxError::InvalidArgument)?;
704 let block_sector = sector + offset as u64;
705 let remaining = sectors - offset;
706 scratch.set_len(size);
707 fill_retirement_markers(scratch.as_mut_slice(), block_sector, remaining);
708
709 let written = unsafe {
710 libc::pwrite(
711 self.fd,
712 scratch.as_ptr() as *const libc::c_void,
713 scratch.len(),
714 (block_sector * FEOX_BLOCK_SIZE as u64) as libc::off_t,
715 )
716 };
717 if written < 0 {
718 return Err(FeoxError::IoError(io::Error::last_os_error()));
719 }
720 if written as usize != size {
721 return Err(FeoxError::IoError(io::Error::new(
722 io::ErrorKind::UnexpectedEof,
723 "Partial write",
724 )));
725 }
726
727 offset += blocks;
728 }
729
730 Ok(())
731 }
732
733 pub fn shutdown(&mut self) {
735 #[cfg(target_os = "linux")]
736 {
737 if let Some(ref mut ring) = self.ring {
738 if ring.submit_and_wait(0).is_ok() {
741 while ring.completion().next().is_some() {
743 }
745 }
746 }
747 self.ring = None;
748 }
749 }
750
751 #[cfg(target_os = "linux")]
754 pub fn batch_write(&mut self, writes: Vec<(u64, Vec<u8>)>) -> Result<()> {
755 self.batch_write_inner(&writes)
756 }
757
758 #[cfg(target_os = "linux")]
759 pub(crate) fn batch_write_bytes(&mut self, writes: &[(u64, Bytes)]) -> Result<()> {
760 self.batch_write_inner(writes)
761 }
762
763 #[cfg(target_os = "linux")]
764 fn batch_write_inner<T: BatchWriteData>(&mut self, writes: &[(u64, T)]) -> Result<()> {
765 self.ensure_writable()?;
766
767 if self.ring.is_none() {
768 for (sector, data) in writes {
769 self.write_sectors_sync(*sector, data.as_slice())?;
770 }
771 self.flush()?;
772 return Ok(());
773 }
774
775 for chunk in writes.chunks(IOURING_MAX_BATCH) {
776 let mut buffers = InFlightBuffers::with_capacity(chunk.len());
777 for (_sector, data) in chunk {
778 if self._use_direct_io {
779 let data = data.as_slice();
780 let mut aligned = AlignedBuffer::new(data.len())?;
781 aligned.set_len(data.len());
782 aligned.as_mut_slice().copy_from_slice(data);
783 buffers.push(PendingWriteBuffer::Aligned(aligned));
784 } else {
785 buffers.push(PendingWriteBuffer::Shared(data.retain_for_write()));
786 }
787 }
788
789 let user_data_base = self.next_user_data;
790 self.next_user_data = self.next_user_data.wrapping_add(chunk.len() as u64);
791
792 let queued = {
793 let ring = self.ring.as_mut().expect("io_uring checked above");
794 let mut sq = ring.submission();
795 let mut queued = 0;
796
797 for (i, (sector, _)) in chunk.iter().enumerate() {
798 let offset = sector * FEOX_BLOCK_SIZE as u64;
799 let buffer = buffers.get(i);
800 let write_e = opcode::Write::new(
801 types::Fd(self.fd),
802 buffer.as_ptr(),
803 buffer.len() as u32,
804 )
805 .offset(offset)
806 .build()
807 .user_data(user_data_base.wrapping_add(i as u64));
808
809 buffers.mark_in_flight(i);
810 if unsafe { sq.push(&write_e) }.is_err() {
811 buffers.mark_unqueued(i);
812 break;
813 }
814 queued += 1;
815 }
816
817 queued
818 };
819
820 let mut first_error =
821 (queued != chunk.len()).then(|| FeoxError::IoError(io::Error::other("SQ full")));
822 let mut completed_count = 0;
823
824 while completed_count < queued {
825 let wait_result = self
826 .ring
827 .as_mut()
828 .expect("io_uring checked above")
829 .submit_and_wait(queued - completed_count);
830
831 if let Err(error) = wait_result {
832 if error.kind() == io::ErrorKind::Interrupted {
833 continue;
834 }
835
836 let submit_error = FeoxError::IndeterminateWrite(error);
837 self.write_indeterminate.store(true, Ordering::Release);
838 mark_file_indeterminate(self.file_identity, &self._file);
839 let ring = self.ring.as_mut().expect("io_uring checked above");
840 process_completions(
841 ring,
842 user_data_base,
843 queued,
844 &mut buffers,
845 &mut completed_count,
846 &mut first_error,
847 );
848 drop(self.ring.take());
849 return Err(submit_error);
850 }
851
852 let ring = self.ring.as_mut().expect("io_uring checked above");
853 process_completions(
854 ring,
855 user_data_base,
856 queued,
857 &mut buffers,
858 &mut completed_count,
859 &mut first_error,
860 );
861 }
862
863 if let Some(error) = first_error {
864 return Err(error);
865 }
866 }
867
868 self.flush()
869 }
870
871 pub fn read_metadata(&self) -> Result<Vec<u8>> {
872 let blocks = self.read_sectors_sync(FEOX_METADATA_BLOCK, FEOX_METADATA_BACKUP_BLOCK + 1)?;
873 let primary = &blocks[..FEOX_BLOCK_SIZE];
874 let backup_start = FEOX_METADATA_BACKUP_BLOCK as usize * FEOX_BLOCK_SIZE;
875 let backup = &blocks[backup_start..backup_start + FEOX_BLOCK_SIZE];
876
877 match (Metadata::from_bytes(primary), Metadata::from_bytes(backup)) {
878 (Some(primary_metadata), Some(backup_metadata))
879 if backup_metadata.generation() > primary_metadata.generation() =>
880 {
881 Ok(backup.to_vec())
882 }
883 (Some(_), _) => Ok(primary.to_vec()),
884 (None, Some(_)) => Ok(backup.to_vec()),
885 (None, None) => Ok(primary.to_vec()),
886 }
887 }
888
889 pub fn write_metadata(&self, metadata: &[u8]) -> Result<()> {
890 let block = metadata_block(metadata)?;
891 self.write_sectors_sync(FEOX_METADATA_BLOCK, &block)?;
892 self.flush()
893 }
894
895 pub(crate) fn write_store_metadata(&self, metadata: &mut Metadata) -> Result<()> {
896 let mut next = *metadata;
897 next.advance_generation()?;
898 let encoded = next.encode();
899 let block = metadata_block(&encoded)?;
900 let sector = if next.generation() & 1 == 0 {
901 FEOX_METADATA_BLOCK
902 } else {
903 FEOX_METADATA_BACKUP_BLOCK
904 };
905 self.write_sectors_sync(sector, &block)?;
906 self.flush()?;
907 *metadata = next;
908 Ok(())
909 }
910
911 pub(crate) fn initialize_store_metadata(&self, metadata: &mut Metadata) -> Result<()> {
912 let mut next = *metadata;
913 next.advance_generation()?;
914 let encoded = next.encode();
915 let block = metadata_block(&encoded)?;
916 self.write_sectors_sync(FEOX_METADATA_BLOCK, &block)?;
917 self.write_sectors_sync(FEOX_METADATA_BACKUP_BLOCK, &block)?;
918 *metadata = next;
919 Ok(())
920 }
921
922 #[cfg(not(target_os = "linux"))]
924 pub fn batch_write(&mut self, writes: Vec<(u64, Vec<u8>)>) -> Result<()> {
925 self.batch_write_inner(&writes)
926 }
927
928 #[cfg(not(target_os = "linux"))]
929 pub(crate) fn batch_write_bytes(&mut self, writes: &[(u64, Bytes)]) -> Result<()> {
930 self.batch_write_inner(writes)
931 }
932
933 #[cfg(not(target_os = "linux"))]
934 fn batch_write_inner<T: BatchWriteData>(&mut self, writes: &[(u64, T)]) -> Result<()> {
935 self.ensure_writable()?;
936 for (sector, data) in writes {
937 self.write_sectors_sync(*sector, data.as_slice())?;
938 }
939 self.flush()?;
940 Ok(())
941 }
942}
943
944fn metadata_block(metadata: &[u8]) -> Result<Vec<u8>> {
945 if metadata.len() > FEOX_BLOCK_SIZE {
946 return Err(FeoxError::InvalidValueSize);
947 }
948
949 let mut block = vec![0; FEOX_BLOCK_SIZE];
950 block[..metadata.len()].copy_from_slice(metadata);
951 Ok(block)
952}
953
954#[cfg(target_os = "linux")]
955fn process_completions(
956 ring: &mut IoUring,
957 user_data_base: u64,
958 queued: usize,
959 buffers: &mut InFlightBuffers<PendingWriteBuffer>,
960 completed_count: &mut usize,
961 first_error: &mut Option<FeoxError>,
962) {
963 for cqe in ring.completion() {
964 let index = cqe.user_data().wrapping_sub(user_data_base);
965 if index >= queued as u64 {
966 continue;
967 }
968 let index = index as usize;
969 if !buffers.mark_complete(index) {
970 continue;
971 }
972
973 *completed_count += 1;
974 if first_error.is_none() {
975 if let Err(error) = validate_write_completion(cqe.result(), buffers.get(index).len()) {
976 *first_error = Some(FeoxError::IoError(error));
977 }
978 }
979 }
980}
981
982#[cfg(test)]
983#[path = "../tests/io_safety_tests.rs"]
984mod tests;