/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; |