Coverage Report

Created: 2026-07-31 00:13

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