Skip to main content

feoxdb/storage/
io.rs

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// Keep poisoned inodes alive so their identities cannot be reused before restart.
89#[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                // A failed io_uring_enter does not prove that the kernel released this pointer.
183                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            // Create io_uring instance
270            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; // Suppress unused warning
311            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, // O_DIRECT not supported on this platform
318            })
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            // Only use aligned buffer for O_DIRECT
340            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                // Return the buffer's data directly (avoids extra copy)
366                Ok(buffer.as_slice().to_vec())
367            } else {
368                // Non-O_DIRECT path: use regular Vec
369                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            // For non-Unix, no O_DIRECT, use regular Vec
400            let mut buffer = vec![0u8; size];
401
402            // For non-Unix, we need platform-specific implementations
403            #[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                // Fallback for other platforms using standard file operations
421                use std::io::{Read, Seek, SeekFrom};
422
423                // Clone the Arc<File> to get a mutable handle for seeking
424                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                // O_DIRECT path: need aligned buffer
448                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                // Non-O_DIRECT path: write directly from input buffer
462                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                // Fallback for other platforms using standard file operations
504                use std::io::{Seek, SeekFrom, Write};
505
506                // Clone the Arc<File> to get a mutable handle for seeking
507                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                // Ensure data is written to disk
519                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    /// Shutdown io_uring to stop SQPOLL kernel thread
734    pub fn shutdown(&mut self) {
735        #[cfg(target_os = "linux")]
736        {
737            if let Some(ref mut ring) = self.ring {
738                // First, wait for any pending submissions to complete
739                // This ensures all in-flight I/O operations finish
740                if ring.submit_and_wait(0).is_ok() {
741                    // Now drain all completions to acknowledge them
742                    while ring.completion().next().is_some() {
743                        // Consume all completion events
744                    }
745                }
746            }
747            self.ring = None;
748        }
749    }
750
751    /// Batch write with io_uring for better throughput
752    /// Operations complete synchronously before returning
753    #[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    /// Non-Linux fallback implementation
923    #[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;