1pub mod simd;
8pub mod capabilities;
10pub mod container;
12pub mod napi;
14
15pub(crate) use simd::*;
16pub use capabilities::*;
17pub use container::*;
18pub use napi::{register_napi_with_ring, unregister_napi_from_ring, try_register_napi, DEFAULT_NAPI_BUSY_POLL_TO_US};
19
20use std::path::{Path, PathBuf};
21use std::os::unix::io::{AsRawFd, RawFd, FromRawFd, IntoRawFd, BorrowedFd};
22use std::sync::atomic::Ordering;
23use std::io;
24use std::ffi::CString;
25use tokio::task::spawn_blocking;
26use io_uring::{opcode, types, IoUring};
27use uuid::Uuid;
28use libc;
29use nix::sys::statfs;
30use std::time::{Duration, Instant};
31use tracing::{warn, debug, error, trace, info};
32use crate::buffer::{BufferPool};
33use crate::error::{FxcpError, Result};
34use crate::security;
35use crate::metrics;
36use std::os::unix::fs::MetadataExt;
37use std::os::unix::ffi::OsStrExt;
38use std::sync::Arc;
39use dashmap::DashMap;
40use crate::governor::Governor;
41use tokio::fs::File;
42use tokio::io::unix::AsyncFd;
43
44const RWF_UNCACHED: i32 = 0x00000040;
45const RWF_ATOMIC: i32 = 0x00000080;
46const OP_TYPE_MASK: u64 = 0xFFFF_0000_0000_0000;
47const INDEX_MASK: u64 = 0x0000_0000_0000_FFFF;
48const READ_OP: u64 = 1 << 48;
49const WRITE_OP: u64 = 2 << 48;
50const FALLOC_OP: u64 = 3 << 48;
51const HOLE_OP: u64 = 4 << 48;
52const POSTCOPY_FSYNC_OP: u64 = 5 << 48;
53const POSTCOPY_RENAME_OP: u64 = 6 << 48;
54const UNLINK_OP: u64 = 8 << 48;
55const LINK_OP: u64 = 9 << 48;
56const SYMLINK_OP: u64 = 10 << 48;
57const TRUNCATE_OP: u64 = 11 << 48;
58const STATX_OP: u64 = 12 << 48;
59const FADVISE_OP: u64 = 13 << 48;
60const SRC_FIXED_SLOT: u32 = 0;
61const DST_FIXED_SLOT: u32 = 1;
62const FICLONE: u64 = crate::constants::FICLONE_IOCTL;
63const FICLONERANGE: u64 = 0x4020940D;
64const F2FS_IOC_START_ATOMIC_WRITE: u64 = 0xF501;
65const F2FS_IOC_COMMIT_ATOMIC_WRITE: u64 = 0xF502;
66const F2FS_IOC_ABORT_ATOMIC_WRITE: u64 = 0xF505;
67const F2FS_IOC_SET_PIN_FILE: u64 = 0xF50D;
68#[allow(dead_code)]
70const BTRFS_IOC_SNAP_CREATE_V2: u64 = 0x50009417;
71#[allow(dead_code)]
72const BTRFS_IOC_SUBVOL_CREATE_V2: u64 = 0x50009418;
73#[allow(dead_code)]
74const BTRFS_IOC_SCRUB: u64 = 0xC400941B;
75#[allow(dead_code)]
76const BTRFS_IOC_SEND: u64 = 0x40489426;
77const BTRFS_IOC_INO_LOOKUP: u64 = 0xD0009412;
78
79#[repr(C)]
80struct FileCloneRange {
81 src_fd: i64,
82 src_offset: u64,
83 src_length: u64,
84 dest_offset: u64,
85}
86
87#[derive(Debug, Clone)]
89pub struct FsyncLatencyTracker {
90 avg_us: u64,
91 deviation_us: u64,
92}
93
94impl Default for FsyncLatencyTracker {
95 fn default() -> Self {
96 Self {
97 avg_us: crate::constants::FSYNC_LATENCY_DEFAULT_AVG_US,
98 deviation_us: crate::constants::FSYNC_LATENCY_DEFAULT_DEVIATION_US,
99 }
100 }
101}
102
103impl FsyncLatencyTracker {
104 pub fn record_success(&mut self, duration: Duration) {
106 let sample = duration.as_micros() as u64;
107 let diff = sample.abs_diff(self.avg_us);
108 self.deviation_us = (self.deviation_us * 3 + diff) / 4;
109 self.avg_us = (self.avg_us * 7 + sample) / 8;
110 }
111
112 pub fn record_timeout(&mut self) {
114 self.avg_us = self.avg_us.saturating_mul(2).min(crate::constants::FSYNC_LATENCY_MAX_US);
115 self.deviation_us = self.deviation_us.saturating_mul(2);
116 }
117
118 pub fn get_timeout(&self) -> Duration {
120 let timeout_us = self.avg_us + (4 * self.deviation_us);
121 Duration::from_micros(timeout_us.clamp(crate::constants::FSYNC_TIMEOUT_MIN_US, crate::constants::FSYNC_TIMEOUT_MAX_US))
122 }
123}
124
125#[allow(dead_code)]
127#[repr(C)]
128pub(crate) struct FileHandle {
129 pub(crate) handle_bytes: u32,
130 pub(crate) handle_type: i32,
131 pub(crate) f_handle: [u8; 128],
132}
133
134#[allow(dead_code)]
135#[repr(C)]
136struct BtrfsIoctlVolArgsV2 {
137 fd: i64,
138 transid: u64,
139 flags: u64,
140 union_reserved: [u64; 4],
141 name: [i8; 4040],
142}
143
144#[allow(dead_code)]
145#[repr(C)]
146struct BtrfsScrubArgs {
147 devid: u64,
148 start: u64,
149 end: u64,
150 flags: u64,
151 progress: [u64; 16],
152}
153
154#[allow(dead_code)]
155#[repr(C)]
156struct BtrfsIoctlSendArgs {
157 send_fd: i64,
158 clone_sources_count: u64,
159 clone_sources: u64,
160 parent_root: u64,
161 flags: u64,
162 reserved: [u64; 4],
163}
164
165#[repr(C)]
166struct BtrfsIoctlInoLookupArgs {
167 treeid: u64,
168 objectid: u64,
169 name: [u8; 4080],
170}
171
172#[derive(Debug, Default, Clone, Copy)]
174pub struct CopyStats {
175 pub bytes_processed: u64,
177 pub bytes_zeros: u64,
179 pub io_duration: Duration,
181 pub ops_count: u64,
183}
184
185#[derive(Debug, Clone)]
186enum FileSegment {
187 Data { offset: u64, len: u64 },
188 Hole { offset: u64, len: u64 },
189}
190
191pub(crate) enum Operation {
192 CopyFile { src: PathBuf, dst: PathBuf, src_file_size: u64, target_label: String, is_sparse: bool },
193 CopyRange { src: PathBuf, dst: PathBuf, offset: u64, length: u64, src_file_size: u64, target_label: String },
194 Truncate { dst: PathBuf, size: u64 },
195 Rename { src: PathBuf, dst: PathBuf, flags: u32 },
196 Fallocate { dst: PathBuf, mode: i32, offset: u64, length: u64 },
197 Unlink { path: PathBuf, is_dir: bool },
198 Link { existing: PathBuf, new: PathBuf },
199 Symlink { target: PathBuf, linkpath: PathBuf },
200}
201
202pub fn reflink_or_copy(src: &Path, dst: &Path) -> io::Result<u64> {
209 use std::os::unix::fs::OpenOptionsExt;
210
211 let src_file = std::fs::File::open(src)?;
212 let dst_file = std::fs::OpenOptions::new()
213 .write(true)
214 .create(true)
215 .truncate(true)
216 .mode(0o644)
217 .open(dst)?;
218
219 let sfd = src_file.as_raw_fd();
220 let dfd = dst_file.as_raw_fd();
221 let size = src_file.metadata()?.len();
222
223 let ret = unsafe { libc::ioctl(dfd, FICLONE as _, sfd) };
226 if ret == 0 {
227 return Ok(size);
228 }
229
230 let mut total: u64 = 0;
232 while total < size {
233 let remaining = size - total;
234 let n = unsafe {
236 libc::sendfile(dfd, sfd, std::ptr::null_mut(), remaining.min(crate::constants::SENDFILE_CHUNK_SIZE) as usize)
237 };
238 match n {
239 -1 => {
240 let err = io::Error::last_os_error();
241 match err.raw_os_error() {
242 Some(libc::EINVAL) | Some(libc::ENOSYS) => break,
243 _ => return Err(err),
244 }
245 }
246 0 => break,
247 n => total += n as u64,
248 }
249 }
250 if total == size {
251 return Ok(size);
252 }
253
254 drop(src_file);
256 drop(dst_file);
257 let mut r = std::fs::File::open(src)?;
258 let mut w = std::fs::OpenOptions::new()
259 .write(true)
260 .create(true)
261 .truncate(true)
262 .open(dst)?;
263 io::copy(&mut r, &mut w)
264}
265
266pub trait OptimizedFs {
268 fn optimized_copy(&mut self, src: PathBuf, dst: PathBuf, src_file_size: u64, target_label: String, buffer_limit: Option<usize>, skip_fsync: bool, is_sparse: bool) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
270 fn optimized_copy_range(&mut self, src: PathBuf, dst: PathBuf, offset: u64, length: u64, src_file_size: u64, target_label: String, buffer_limit: Option<usize>, skip_fsync: bool) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
272 fn optimized_truncate(&mut self, dst: PathBuf, size: u64) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
274 fn optimized_rename(&mut self, src: PathBuf, dst: PathBuf, flags: u32) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
276 fn optimized_fallocate(&mut self, dst: PathBuf, mode: i32, offset: u64, length: u64) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
278 fn optimized_unlink(&mut self, path: PathBuf, is_dir: bool) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
281 fn optimized_link(&mut self, existing: PathBuf, new: PathBuf) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
284 fn optimized_symlink(&mut self, target: PathBuf, linkpath: PathBuf) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
287}
288
289pub(crate) struct CleanupGuard {
290 files: Vec<PathBuf>,
291}
292
293impl CleanupGuard {
294 pub(crate) fn new(files: Vec<PathBuf>) -> Self {
295 Self { files }
296 }
297
298 #[allow(dead_code)]
299 pub(crate) fn empty() -> Self {
300 Self { files: Vec::new() }
301 }
302
303 #[allow(dead_code)]
304 pub fn register(&mut self, path: PathBuf) {
305 self.files.push(path);
306 }
307
308 #[allow(dead_code)]
309 pub fn disarm(&mut self, path: &Path) {
310 self.files.retain(|p| p != path);
311 }
312}
313
314impl Drop for CleanupGuard {
315 fn drop(&mut self) {
316 for p in &self.files {
317 let _ = std::fs::remove_file(p);
318 }
319 }
320}
321
322pub fn btrfs_resolve_inode(fd: RawFd, inode: u64) -> Result<PathBuf> {
324 let mut args = BtrfsIoctlInoLookupArgs { treeid: 0, objectid: inode, name: [0; 4080] };
325 let ret = unsafe { libc::ioctl(fd, BTRFS_IOC_INO_LOOKUP, &mut args) };
328 if ret < 0 { return Err(FxcpError::Io(std::io::Error::last_os_error())); }
329
330 let name_slice = match args.name.iter().position(|&c| c == 0) {
331 Some(pos) => &args.name[..pos],
332 None => &args.name[..],
333 };
334
335 Ok(PathBuf::from(std::ffi::OsStr::from_bytes(name_slice)))
336}
337
338#[allow(dead_code)]
339pub(crate) fn btrfs_create_snapshot(src_fd: RawFd, dest_dir: &Path, name: &str) -> Result<()> {
340 let dest_dir_file = std::fs::File::open(dest_dir).map_err(FxcpError::Io)?;
341 let cname = CString::new(name).map_err(|_| FxcpError::Config("Invalid snapshot name".into()))?;
342
343 if cname.as_bytes().len() > 4039 {
344 return Err(FxcpError::Config("Snapshot name too long".into()));
345 }
346
347 let mut args = BtrfsIoctlVolArgsV2 {
348 fd: src_fd as i64,
349 transid: 0,
350 flags: 0,
351 union_reserved: [0; 4],
352 name: [0; 4040]
353 };
354
355 unsafe {
358 std::ptr::copy_nonoverlapping(
359 cname.as_bytes_with_nul().as_ptr(),
360 args.name.as_mut_ptr().cast::<u8>(),
361 cname.as_bytes_with_nul().len()
362 );
363 }
364
365 let ret = unsafe { libc::ioctl(dest_dir_file.as_raw_fd(), BTRFS_IOC_SNAP_CREATE_V2, &args) };
368 if ret < 0 {
369 return Err(FxcpError::Io(std::io::Error::last_os_error()));
370 }
371 Ok(())
372}
373
374#[allow(dead_code)]
375pub(crate) fn btrfs_create_subvol(dest_dir: &Path, name: &str) -> Result<()> {
376 let dir_file = std::fs::File::open(dest_dir).map_err(FxcpError::Io)?;
377 let cname = CString::new(name).map_err(|_| FxcpError::Config("Invalid subvol name".into()))?;
378
379 if cname.as_bytes().len() > 4039 {
380 return Err(FxcpError::Config("Subvolume name too long".into()));
381 }
382
383 let mut args = BtrfsIoctlVolArgsV2 {
384 fd: 0,
385 transid: 0,
386 flags: 0,
387 union_reserved: [0; 4],
388 name: [0; 4040]
389 };
390
391 unsafe {
394 std::ptr::copy_nonoverlapping(
395 cname.as_bytes_with_nul().as_ptr(),
396 args.name.as_mut_ptr().cast::<u8>(),
397 cname.as_bytes_with_nul().len()
398 );
399 }
400
401 let ret = unsafe { libc::ioctl(dir_file.as_raw_fd(), BTRFS_IOC_SUBVOL_CREATE_V2, &args) };
404 if ret < 0 {
405 return Err(FxcpError::Io(std::io::Error::last_os_error()));
406 }
407 Ok(())
408}
409
410#[allow(dead_code)]
411pub(crate) fn btrfs_scrub_start(mount_point: &Path) -> Result<()> {
412 let f = std::fs::File::open(mount_point).map_err(FxcpError::Io)?;
413 let mut args: BtrfsScrubArgs = unsafe { std::mem::zeroed() };
416 args.devid = 0;
417
418 let ret = unsafe { libc::ioctl(f.as_raw_fd(), BTRFS_IOC_SCRUB, &mut args) };
420 if ret < 0 {
421 let err = std::io::Error::last_os_error();
422 if err.raw_os_error() == Some(libc::EINPROGRESS) {
423 return Ok(());
424 }
425 return Err(FxcpError::Io(err));
426 }
427 Ok(())
428}
429
430#[allow(dead_code)]
431pub(crate) fn btrfs_send_stream(
432 subvol_fd: RawFd,
433 parent_root_id: u64,
434 clone_sources: &[u64]
435) -> Result<std::fs::File> {
436 let (pipe_r, pipe_w) = nix::unistd::pipe().map_err(FxcpError::System)?;
437 let clone_sources_vec = clone_sources.to_vec();
438
439 let fd_dup = nix::unistd::dup(unsafe { BorrowedFd::borrow_raw(subvol_fd) }).map_err(FxcpError::System)?;
441 let pipe_w_fd = pipe_w.into_raw_fd();
442
443 std::thread::spawn(move || {
444 let ptr = if !clone_sources_vec.is_empty() {
445 clone_sources_vec.as_ptr() as u64
446 } else {
447 0
448 };
449
450 let mut args = BtrfsIoctlSendArgs {
451 send_fd: pipe_w_fd as i64,
452 clone_sources_count: clone_sources_vec.len() as u64,
453 clone_sources: ptr,
454 parent_root: parent_root_id,
455 flags: 0,
456 reserved: [0; 4]
457 };
458
459 let ret = unsafe { libc::ioctl(fd_dup.as_raw_fd(), BTRFS_IOC_SEND, &mut args) };
462 drop(fd_dup);
463 unsafe { libc::close(pipe_w_fd); }
465
466 if ret < 0 {
467 error!("Btrfs Send Failed: {}", std::io::Error::last_os_error());
468 } else {
469 debug!("Btrfs Send Completed successfully.");
470 }
471 });
472
473 let file = unsafe { std::fs::File::from_raw_fd(pipe_r.into_raw_fd()) };
476 Ok(file)
477}
478
479#[allow(dead_code)]
480pub(crate) fn open_by_handle_at(mount_fd: RawFd, handle: &FileHandle) -> Result<std::fs::File> {
481 let fd = unsafe {
484 libc::syscall(
485 libc::SYS_open_by_handle_at,
486 mount_fd,
487 handle as *const _ as *const libc::c_void,
488 libc::O_RDONLY | libc::O_NOATIME
489 )
490 };
491 if fd < 0 {
492 return Err(FxcpError::Io(std::io::Error::last_os_error()));
493 }
494 Ok(unsafe { std::fs::File::from_raw_fd(fd as i32) })
497}
498
499pub struct SmartCopier {
504 pub ring: IoUring,
506 pub buffer_pool: BufferPool,
508 pub atomic_buffer_pool: Option<BufferPool>,
510 pub async_fd: Arc<AsyncFd<RawFd>>,
512 pub vdo_opt: bool,
514 pub direct_io_ok: bool,
516 pub source_caps: Arc<Capabilities>,
518 pub target_caps: Arc<Capabilities>,
520 pub vdo_stall_threshold: u32,
522 pub barrier_callback: Option<Box<dyn Fn(u64) + Send + Sync>>,
524 pub source_uncached: bool,
526 pub target_uncached: bool,
528 pub governor: Option<Arc<Governor>>,
530 pub fsync_tracker: FsyncLatencyTracker,
532 pub skip_fsync: bool,
534 pub segment_stall_timeout_secs: u64,
536 pub segment_overall_timeout_secs: u64,
538 pub postcopy_ring: IoUring,
541 pub postcopy_async_fd: Arc<AsyncFd<RawFd>>,
543}
544
545impl OptimizedFs for SmartCopier {
546 fn optimized_copy(&mut self, src: PathBuf, dst: PathBuf, src_file_size: u64, target_label: String, buffer_limit: Option<usize>, skip_fsync: bool, is_sparse: bool) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
547 async move {
548 let op = Operation::CopyFile { src, dst, src_file_size, target_label, is_sparse };
549 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, buffer_limit, skip_fsync, self.segment_stall_timeout_secs, self.segment_overall_timeout_secs, &mut self.postcopy_ring, &self.postcopy_async_fd).await
550 }
551 }
552
553 fn optimized_rename(&mut self, src: PathBuf, dst: PathBuf, flags: u32) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
554 async move {
555 let op = Operation::Rename { src, dst, flags };
556 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, None, false, 60, 600, &mut self.postcopy_ring, &self.postcopy_async_fd).await
557 }
558 }
559
560 fn optimized_copy_range(&mut self, src: PathBuf, dst: PathBuf, offset: u64, length: u64, src_file_size: u64, target_label: String, buffer_limit: Option<usize>, skip_fsync: bool) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
561 async move {
562 let op = Operation::CopyRange { src, dst, offset, length, src_file_size, target_label };
563 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, buffer_limit, skip_fsync, self.segment_stall_timeout_secs, self.segment_overall_timeout_secs, &mut self.postcopy_ring, &self.postcopy_async_fd).await
564 }
565 }
566
567 fn optimized_truncate(&mut self, dst: PathBuf, size: u64) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
568 async move {
569 let op = Operation::Truncate { dst, size };
570 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, None, false, 60, 600, &mut self.postcopy_ring, &self.postcopy_async_fd).await
571 }
572 }
573
574 fn optimized_fallocate(&mut self, dst: PathBuf, mode: i32, offset: u64, length: u64) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
575 async move {
576 let op = Operation::Fallocate { dst, mode, offset, length };
577 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, None, false, 60, 600, &mut self.postcopy_ring, &self.postcopy_async_fd).await
578 }
579 }
580
581 fn optimized_unlink(&mut self, path: PathBuf, is_dir: bool) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
582 async move {
583 let op = Operation::Unlink { path, is_dir };
584 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, None, false, 60, 600, &mut self.postcopy_ring, &self.postcopy_async_fd).await
585 }
586 }
587
588 fn optimized_link(&mut self, existing: PathBuf, new: PathBuf) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
589 async move {
590 let op = Operation::Link { existing, new };
591 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, None, false, 60, 600, &mut self.postcopy_ring, &self.postcopy_async_fd).await
592 }
593 }
594
595 fn optimized_symlink(&mut self, target: PathBuf, linkpath: PathBuf) -> impl std::future::Future<Output = Result<CopyStats>> + Send {
596 async move {
597 let op = Operation::Symlink { target, linkpath };
598 Self::dispatch(op, &mut self.ring, &mut self.buffer_pool, self.atomic_buffer_pool.as_mut(), self.async_fd.clone(), self.vdo_opt, self.direct_io_ok, &self.source_caps, &self.target_caps, self.vdo_stall_threshold, &self.barrier_callback, self.source_uncached, self.target_uncached, self.governor.clone(), &mut self.fsync_tracker, None, false, 60, 600, &mut self.postcopy_ring, &self.postcopy_async_fd).await
599 }
600 }
601}
602
603impl SmartCopier {
604 pub async fn optimized_statx_nlink(&mut self, path: PathBuf) -> Result<u64> {
608 let path_cstr = CString::new(path.as_os_str().as_bytes())
609 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
610
611 let mut statx_buf: libc::statx = unsafe { std::mem::zeroed() };
613
614 let sqe = opcode::Statx::new(
615 types::Fd(libc::AT_FDCWD),
616 path_cstr.as_ptr(),
617 &mut statx_buf as *mut libc::statx as *mut _,
618 )
619 .flags(0)
620 .mask(libc::STATX_NLINK)
621 .build()
622 .user_data(STATX_OP);
623
624 match Self::submit_and_wait_single(&mut self.postcopy_ring, sqe, &self.postcopy_async_fd).await {
625 Ok(_) => Ok(statx_buf.stx_nlink as u64),
626 Err(FxcpError::Io(ref e))
627 if e.raw_os_error() == Some(libc::EOPNOTSUPP)
628 || e.raw_os_error() == Some(libc::EINVAL) =>
629 {
630 debug!("io_uring Statx unsupported for {}, falling back to spawn_blocking", path.display());
631 let meta = spawn_blocking(move || std::fs::metadata(&path))
632 .await
633 .map_err(FxcpError::Join)?
634 .map_err(FxcpError::Io)?;
635 Ok(meta.nlink())
636 }
637 Err(e) => Err(e),
638 }
639 }
640}
641
642pub(crate) fn prepare_target_parent(target: &Path) -> std::io::Result<()> {
644 if let Some(parent) = target.parent()
645 && !parent.exists() {
646 std::fs::create_dir_all(parent)?;
647 }
648 Ok(())
649}
650
651fn sendfile_full(sfd: RawFd, dfd: RawFd, size: u64, label: &str) -> Result<CopyStats> {
657 let start = Instant::now();
658 let mut total: u64 = 0;
659 while total < size {
660 let remaining = size - total;
661 let ret = unsafe {
663 libc::sendfile(dfd, sfd, std::ptr::null_mut(), remaining.min(crate::constants::SENDFILE_CHUNK_SIZE) as usize)
664 };
665 if ret < 0 {
666 return Err(FxcpError::Io(std::io::Error::last_os_error()));
667 }
668 if ret == 0 { break; }
669 total += ret as u64;
670 }
671 if total == size {
672 Ok(CopyStats {
673 bytes_processed: size,
674 bytes_zeros: 0,
675 io_duration: start.elapsed(),
676 ops_count: 1,
677 })
678 } else {
679 Err(FxcpError::Io(io::Error::new(
680 io::ErrorKind::UnexpectedEof,
681 format!("{}: {} of {} bytes", label, total, size),
682 )))
683 }
684}
685
686unsafe fn build_write_sqe(
695 use_fixed: bool,
696 dfd: RawFd,
697 ptr: *const u8,
698 len: u32,
699 buf_index: u16,
700 offset: u64,
701 rw_flags: i32,
702 user_data: u64,
703) -> io_uring::squeue::Entry {
704 if use_fixed {
705 opcode::WriteFixed::new(types::Fixed(DST_FIXED_SLOT), ptr, len, buf_index)
706 .offset(offset).rw_flags(rw_flags).build().user_data(user_data)
707 } else {
708 opcode::Write::new(types::Fd(dfd), ptr, len)
709 .offset(offset).rw_flags(rw_flags).build().user_data(user_data)
710 }
711}
712
713async fn try_nfs_sendfile(
716 sfd: RawFd,
717 dfd: RawFd,
718 src_file_size: u64,
719 is_sparse: bool,
720 target_is_nfs: bool,
721 target_label: &str,
722) -> Result<Option<CopyStats>> {
723 if is_sparse || !target_is_nfs || src_file_size < crate::constants::NFS_SENDFILE_MIN_SIZE {
724 return Ok(None);
725 }
726
727 let sf_size = src_file_size;
728 let sendfile_res = spawn_blocking(move || {
729 sendfile_full(sfd, dfd, sf_size, "NFS sendfile")
730 }).await.map_err(FxcpError::Join)?;
731
732 match sendfile_res {
733 Ok(sf_stats) => {
734 metrics::COPY_METHOD_OFFLOAD.with_label_values(&[target_label]).inc();
735 debug!("execute_full_copy_logic: NFS sendfile fast-path ({} bytes)", src_file_size);
736 Ok(Some(sf_stats))
737 }
738 Err(e) => {
739 debug!("execute_full_copy_logic: NFS sendfile failed ({}), io_uring fallback", e);
740 unsafe {
742 libc::lseek(sfd, 0, libc::SEEK_SET);
743 libc::lseek(dfd, 0, libc::SEEK_SET);
744 };
745 Ok(None)
746 }
747 }
748}
749
750async fn try_cachestat_sendfile(
753 sfd: RawFd,
754 dfd: RawFd,
755 src_file_size: u64,
756 is_sparse: bool,
757 target_label: &str,
758) -> Result<Option<CopyStats>> {
759 const CACHESTAT_SIZE_HIGH: u64 = 1_048_576;
760 if is_sparse
761 || !(crate::constants::NFS_SENDFILE_MIN_SIZE..=CACHESTAT_SIZE_HIGH).contains(&src_file_size)
762 {
763 return Ok(None);
764 }
765
766 let residency = match query_cache_residency(sfd, src_file_size) {
767 Some(r) if r > crate::constants::CACHE_RESIDENCY_SENDFILE_THRESHOLD => r,
768 Some(r) => {
769 debug!("execute_full_copy_logic: cachestat {:.0}% cached -- preferring io_uring", r * 100.0);
770 return Ok(None);
771 }
772 None => return Ok(None),
773 };
774
775 debug!("execute_full_copy_logic: cachestat {:.0}% cached -- sendfile fast-path", residency * 100.0);
776 let sf_size = src_file_size;
777 let sendfile_res = spawn_blocking(move || {
778 sendfile_full(sfd, dfd, sf_size, "sendfile")
779 }).await.map_err(FxcpError::Join)?;
780
781 match sendfile_res {
782 Ok(sf_stats) => {
783 metrics::COPY_METHOD_OFFLOAD.with_label_values(&[target_label]).inc();
784 Ok(Some(sf_stats))
785 }
786 Err(e) => {
787 debug!("execute_full_copy_logic: sendfile failed ({}), io_uring fallback", e);
788 unsafe { libc::lseek(sfd, 0, libc::SEEK_SET); libc::lseek(dfd, 0, libc::SEEK_SET); };
790 Ok(None)
791 }
792 }
793}
794
795async fn uring_fsync_with_timeout(
798 ring: &mut IoUring,
799 dfd: RawFd,
800 use_fixed: bool,
801 async_fd: &AsyncFd<RawFd>,
802 fsync_tracker: &mut FsyncLatencyTracker,
803 target_path: &Path,
804 skip_fsync: bool,
805) -> Result<()> {
806 if skip_fsync {
807 ring.submit()?;
808 return Ok(());
809 }
810
811 let fsync_op = if use_fixed {
812 opcode::Fsync::new(types::Fixed(DST_FIXED_SLOT)).build().user_data(0)
813 } else {
814 opcode::Fsync::new(types::Fd(dfd)).build().user_data(0)
815 };
816 let mut pushed = false;
817
818 for _ in 0..10 {
819 if unsafe { ring.submission().push(&fsync_op) }.is_ok() {
821 pushed = true;
822 break;
823 }
824 let _ = ring.submit();
825 if ring.submission().is_full() {
826 match tokio::time::timeout(Duration::from_millis(crate::constants::IO_URING_READABLE_TIMEOUT_MS), async_fd.readable()).await {
827 Ok(Ok(mut guard)) => {
828 let mut buf = [0u8; 8];
829 let _ = unsafe { libc::read(async_fd.get_ref().as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, 8) };
831 guard.clear_ready();
832 },
833 Ok(Err(e)) => return Err(FxcpError::Io(e)),
834 Err(_) => {
835 if let Some(_) = ring.completion().next() {
836 }
838 }
839 }
840 }
841 }
842
843 if !pushed {
844 let _ = ring.submit();
845 unsafe { ring.submission().push(&fsync_op) }.map_err(|e| FxcpError::Io(io::Error::other(e.to_string())))?;
847 }
848
849 ring.submit()?;
850
851 let mut fsync_found = false;
852 let mut last_log = Instant::now();
853 let fsync_start = Instant::now();
854 let timeout_duration = fsync_tracker.get_timeout();
855 let fsync_deadline = Instant::now() + timeout_duration;
856
857 while !fsync_found {
858 if let Some(c) = ring.completion().next() {
859 if c.user_data() == 0 {
860 if c.result() < 0 {
861 return Err(FxcpError::Io(io::Error::from_raw_os_error(-c.result())));
862 }
863 fsync_found = true;
864 }
865 continue;
866 }
867
868 if Instant::now() > fsync_deadline {
869 warn!("io_uring fsync timed out on {:?} (> {:?}). Ramp-up triggered. Falling back to blocking fsync.", target_path, timeout_duration);
870 fsync_tracker.record_timeout();
871 unsafe { libc::fsync(dfd) };
873 break;
874 }
875
876 match tokio::time::timeout(Duration::from_millis(crate::constants::IO_URING_READABLE_TIMEOUT_MS), async_fd.readable()).await {
877 Ok(Ok(mut guard)) => {
878 let mut buf = [0u8; 8];
879 let _ = unsafe { libc::read(async_fd.get_ref().as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, 8) };
881 guard.clear_ready();
882 },
883 Ok(Err(e)) => return Err(FxcpError::Io(e)),
884 Err(_) => {
885 let _ = ring.submit();
886 if let Some(_) = ring.completion().next() {
887 }
889 }
890 }
891
892 if last_log.elapsed() > Duration::from_secs(crate::constants::LOG_THROTTLE_INTERVAL_SECS) {
893 info!("Still waiting for fsync completion on {:?} (Elapsed: {:?}, Timeout: {:?})...", target_path, fsync_start.elapsed(), timeout_duration);
894 last_log = Instant::now();
895 }
896 }
897 if fsync_found {
898 fsync_tracker.record_success(fsync_start.elapsed());
899 }
900
901 Ok(())
902}
903
904fn handle_read_completion(
908 ring: &mut IoUring,
909 active_pool: &mut BufferPool,
910 index: u16,
911 bytes_transferred: u64,
912 original_offset: u64,
913 dfd: RawFd,
914 use_fixed: bool,
915 vdo_opt: bool,
916 write_flags: i32,
917) -> Result<(u64, u64)> {
918 if let Err(e) = active_pool.set_len(index, bytes_transferred as usize) {
919 error!("io_uring buffer set_len failed: {}", e);
920 active_pool.release(index);
921 return Err(e);
922 }
923
924 let ptr = match active_pool.get_ptr(index) {
925 Some(ptr) => ptr,
926 None => {
927 error!("io_uring buffer pool index out of range (write dispatch): index={}", index);
928 active_pool.release(index);
929 return Err(FxcpError::Io(io::Error::other("io_uring buffer pool index out of range (write dispatch)")));
930 }
931 };
932
933 let is_zero_block = if vdo_opt && bytes_transferred > 0 {
934 let s = unsafe { std::slice::from_raw_parts(ptr, bytes_transferred as usize) };
937 is_zero_block(s)
938 } else { false };
939
940 if is_zero_block && bytes_transferred >= crate::constants::MIN_HOLE_PUNCH_SIZE {
941 let user_data_falloc = FALLOC_OP | (index as u64);
942 let falloc_op = if use_fixed {
943 opcode::Fallocate::new(types::Fixed(DST_FIXED_SLOT), bytes_transferred)
944 .offset(original_offset)
945 .mode(libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE)
946 .build().user_data(user_data_falloc)
947 } else {
948 opcode::Fallocate::new(types::Fd(dfd), bytes_transferred)
949 .offset(original_offset)
950 .mode(libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE)
951 .build().user_data(user_data_falloc)
952 };
953
954 if unsafe { ring.submission().push(&falloc_op) }.is_ok() {
956 return Ok((bytes_transferred, bytes_transferred));
957 } else {
958 active_pool.release(index);
959 return Err(FxcpError::Io(io::Error::other("Submission queue full during zero-block fallocate")));
960 }
961 }
962
963 let user_data_write = WRITE_OP | (index as u64);
964 let write_op = unsafe { build_write_sqe(use_fixed, dfd, ptr, bytes_transferred as u32, index, original_offset, write_flags, user_data_write) };
966
967 if unsafe { ring.submission().push(&write_op) }.is_ok() {
969 Ok((0, 0))
970 } else {
971 active_pool.release(index);
972 Err(FxcpError::Io(io::Error::other("Submission queue full during write")))
973 }
974}
975
976impl SmartCopier {
977 async fn open_source_noatime(path: &Path) -> Result<File> {
978 let mut opts = tokio::fs::OpenOptions::new();
979 opts.read(true);
980 opts.custom_flags(libc::O_NOATIME);
981 let file = match opts.open(path).await {
982 Ok(f) => f,
983 Err(e) => {
984 if let Some(raw) = e.raw_os_error()
985 && (raw == libc::EPERM || raw == libc::EACCES) {
986 return File::open(path).await.map_err(FxcpError::Io);
987 }
988 return Err(FxcpError::Io(e));
989 }
990 };
991 let ret = unsafe {
995 libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL)
996 };
997 if ret != 0 {
998 debug!("posix_fadvise(SEQUENTIAL) failed: {}", std::io::Error::from_raw_os_error(ret));
999 }
1000 Ok(file)
1001 }
1002
1003 fn set_f2fs_pinning(fd: RawFd, enable: bool) {
1004 let val: u32 = if enable { 1 } else { 0 };
1005 let _ = unsafe { libc::ioctl(fd, F2FS_IOC_SET_PIN_FILE, &val) };
1008 }
1009
1010 pub async fn copy(
1012 src: &Path,
1013 dst: &Path,
1014 ring: &mut IoUring,
1015 buffer_pool: &mut BufferPool,
1016 atomic_pool: Option<&mut BufferPool>,
1017 async_fd: Arc<AsyncFd<RawFd>>,
1018 vdo_opt: bool,
1019 offset: u64,
1020 length: u64,
1021 direct_io_ok: bool,
1022 src_file_size: u64,
1023 source_caps: &Arc<Capabilities>,
1024 target_caps: &Arc<Capabilities>,
1025 vdo_stall_threshold: u32,
1026 source_uncached: bool,
1027 target_uncached: bool,
1028 barrier_callback: &Option<Box<dyn Fn(u64) + Send + Sync>>,
1029 governor: Option<Arc<Governor>>,
1030 target_label: String,
1031 fsync_tracker: &mut FsyncLatencyTracker,
1032 skip_fsync: bool,
1033 segment_stall_timeout_secs: u64,
1034 segment_overall_timeout_secs: u64,
1035 is_sparse: bool,
1036 postcopy_ring: &mut IoUring,
1037 postcopy_async_fd: &Arc<AsyncFd<RawFd>>,
1038 ) -> Result<CopyStats> {
1039 Self::copy_with_limit(src, dst, ring, buffer_pool, atomic_pool, async_fd, vdo_opt, offset, length, direct_io_ok, src_file_size, source_caps, target_caps, vdo_stall_threshold, source_uncached, target_uncached, barrier_callback, governor, target_label, fsync_tracker, None, skip_fsync, segment_stall_timeout_secs, segment_overall_timeout_secs, is_sparse, postcopy_ring, postcopy_async_fd).await
1040 }
1041
1042 pub async fn copy_with_limit(
1044 src: &Path,
1045 dst: &Path,
1046 ring: &mut IoUring,
1047 buffer_pool: &mut BufferPool,
1048 atomic_pool: Option<&mut BufferPool>,
1049 async_fd: Arc<AsyncFd<RawFd>>,
1050 vdo_opt: bool,
1051 offset: u64,
1052 length: u64,
1053 direct_io_ok: bool,
1054 src_file_size: u64,
1055 source_caps: &Arc<Capabilities>,
1056 target_caps: &Arc<Capabilities>,
1057 vdo_stall_threshold: u32,
1058 source_uncached: bool,
1059 target_uncached: bool,
1060 barrier_callback: &Option<Box<dyn Fn(u64) + Send + Sync>>,
1061 governor: Option<Arc<Governor>>,
1062 target_label: String,
1063 fsync_tracker: &mut FsyncLatencyTracker,
1064 buffer_limit: Option<usize>,
1065 skip_fsync: bool,
1066 segment_stall_timeout_secs: u64,
1067 segment_overall_timeout_secs: u64,
1068 is_sparse: bool,
1069 postcopy_ring: &mut IoUring,
1070 postcopy_async_fd: &Arc<AsyncFd<RawFd>>,
1071 ) -> Result<CopyStats> {
1072 if !src.exists() {
1073 return Err(FxcpError::Io(std::io::Error::new(
1074 std::io::ErrorKind::NotFound,
1075 format!("Source file no longer exists: {:?}", src)
1076 )));
1077 }
1078
1079 if offset == 0 && length == src_file_size {
1080 Self::execute_copy_file(
1081 src, dst, ring, buffer_pool, atomic_pool, async_fd, vdo_opt, direct_io_ok,
1082 source_caps, target_caps, src_file_size, vdo_stall_threshold,
1083 barrier_callback, source_uncached, target_uncached, governor,
1084 target_label, fsync_tracker, buffer_limit, skip_fsync,
1085 segment_stall_timeout_secs, segment_overall_timeout_secs,
1086 is_sparse,
1087 postcopy_ring, postcopy_async_fd,
1088 ).await
1089 } else {
1090 Self::execute_copy_range(
1091 src, dst, ring, buffer_pool, atomic_pool, async_fd, vdo_opt, offset, length,
1092 direct_io_ok, src_file_size, source_caps, target_caps,
1093 vdo_stall_threshold, source_uncached, target_uncached, governor,
1094 target_label, fsync_tracker, buffer_limit, skip_fsync,
1095 segment_stall_timeout_secs, segment_overall_timeout_secs
1096 ).await
1097 }
1098 }
1099
1100 pub(crate) async fn dispatch(
1102 op: Operation,
1103 ring: &mut IoUring,
1104 buffer_pool: &mut BufferPool,
1105 atomic_pool: Option<&mut BufferPool>,
1106 async_fd: Arc<AsyncFd<RawFd>>,
1107 vdo_opt: bool,
1108 direct_io_ok: bool,
1109 source_caps: &Arc<Capabilities>,
1110 target_caps: &Arc<Capabilities>,
1111 vdo_stall_threshold: u32,
1112 barrier_callback: &Option<Box<dyn Fn(u64) + Send + Sync>>,
1113 source_uncached: bool,
1114 target_uncached: bool,
1115 governor: Option<Arc<Governor>>,
1116 fsync_tracker: &mut FsyncLatencyTracker,
1117 buffer_limit: Option<usize>,
1118 skip_fsync: bool,
1119 segment_stall_timeout_secs: u64,
1120 segment_overall_timeout_secs: u64,
1121 postcopy_ring: &mut IoUring,
1122 postcopy_async_fd: &Arc<AsyncFd<RawFd>>,
1123 ) -> Result<CopyStats> {
1124 match op {
1125 Operation::CopyFile { src, dst, src_file_size, target_label, is_sparse } => {
1126 Self::execute_copy_file(
1127 &src, &dst, ring, buffer_pool, atomic_pool, async_fd, vdo_opt,
1128 direct_io_ok, source_caps, target_caps,
1129 src_file_size, vdo_stall_threshold, barrier_callback,
1130 source_uncached, target_uncached, governor,
1131 target_label, fsync_tracker, buffer_limit, skip_fsync,
1132 segment_stall_timeout_secs, segment_overall_timeout_secs,
1133 is_sparse,
1134 postcopy_ring, postcopy_async_fd,
1135 ).await
1136 },
1137 Operation::CopyRange { src, dst, offset, length, src_file_size, target_label } => {
1138 Self::execute_copy_range(
1139 &src, &dst, ring, buffer_pool, atomic_pool, async_fd, vdo_opt,
1140 offset, length, direct_io_ok, src_file_size,
1141 source_caps, target_caps, vdo_stall_threshold,
1142 source_uncached, target_uncached, governor,
1143 target_label, fsync_tracker, buffer_limit, skip_fsync,
1144 segment_stall_timeout_secs, segment_overall_timeout_secs
1145 ).await
1146 },
1147 Operation::Truncate { dst, size } => {
1148 let f = std::fs::OpenOptions::new()
1149 .write(true)
1150 .open(&dst)
1151 .map_err(FxcpError::Io)?;
1152 let sqe = opcode::Ftruncate::new(types::Fd(f.as_raw_fd()), size)
1153 .build()
1154 .user_data(TRUNCATE_OP);
1155 match Self::submit_and_wait_single(postcopy_ring, sqe, postcopy_async_fd).await {
1156 Ok(_) => Ok(CopyStats::default()),
1157 Err(FxcpError::Io(ref e)) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => {
1158 drop(f);
1159 spawn_blocking(move || {
1160 security::truncate_file(&dst, size).map(|_| CopyStats::default())
1161 }).await.map_err(FxcpError::Join)?
1162 }
1163 Err(e) => Err(e),
1164 }
1165 },
1166 Operation::Rename { src, dst, flags } => {
1167 spawn_blocking(move || {
1168 crate::consistency::atomic_rename(&src, &dst, Some(flags)).map(|_| CopyStats::default()).map_err(FxcpError::Io)
1169 }).await.map_err(FxcpError::Join)?},
1170 Operation::Fallocate { dst, mode, offset, length } => {
1171 spawn_blocking(move || {
1172 security::do_fallocate(&dst, offset, length, mode).map(|_| CopyStats::default())
1173 }).await.map_err(FxcpError::Join)?
1174 },
1175 Operation::Unlink { path, is_dir } => {
1176 let c_path = CString::new(path.as_os_str().as_bytes())
1177 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
1178 let flags = if is_dir { libc::AT_REMOVEDIR } else { 0 };
1179 let sqe = opcode::UnlinkAt::new(types::Fd(libc::AT_FDCWD), c_path.as_ptr())
1180 .flags(flags)
1181 .build()
1182 .user_data(UNLINK_OP);
1183 match Self::submit_and_wait_single(postcopy_ring, sqe, postcopy_async_fd).await {
1184 Ok(_) => Ok(CopyStats::default()),
1185 Err(FxcpError::Io(ref e)) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => {
1186 spawn_blocking(move || {
1187 if is_dir { std::fs::remove_dir(&path) } else { std::fs::remove_file(&path) }
1188 .map(|_| CopyStats::default())
1189 .map_err(FxcpError::Io)
1190 }).await.map_err(FxcpError::Join)?
1191 }
1192 Err(e) => Err(e),
1193 }
1194 },
1195 Operation::Link { existing, new } => {
1196 let old_cstr = CString::new(existing.as_os_str().as_bytes())
1197 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
1198 let new_cstr = CString::new(new.as_os_str().as_bytes())
1199 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
1200 let sqe = opcode::LinkAt::new(
1201 types::Fd(libc::AT_FDCWD), old_cstr.as_ptr(),
1202 types::Fd(libc::AT_FDCWD), new_cstr.as_ptr(),
1203 ).build().user_data(LINK_OP);
1204 match Self::submit_and_wait_single(postcopy_ring, sqe, postcopy_async_fd).await {
1205 Ok(_) => Ok(CopyStats::default()),
1206 Err(FxcpError::Io(ref e)) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => {
1207 spawn_blocking(move || {
1208 std::fs::hard_link(&existing, &new)
1209 .map(|_| CopyStats::default())
1210 .map_err(FxcpError::Io)
1211 }).await.map_err(FxcpError::Join)?
1212 }
1213 Err(e) => Err(e),
1214 }
1215 },
1216 Operation::Symlink { target, linkpath } => {
1217 let target_cstr = CString::new(target.as_os_str().as_bytes())
1218 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
1219 let link_cstr = CString::new(linkpath.as_os_str().as_bytes())
1220 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
1221 let sqe = opcode::SymlinkAt::new(
1222 types::Fd(libc::AT_FDCWD),
1223 target_cstr.as_ptr(),
1224 link_cstr.as_ptr(),
1225 ).build().user_data(SYMLINK_OP);
1226 match Self::submit_and_wait_single(postcopy_ring, sqe, postcopy_async_fd).await {
1227 Ok(_) => Ok(CopyStats::default()),
1228 Err(FxcpError::Io(ref e)) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => {
1229 spawn_blocking(move || {
1230 std::os::unix::fs::symlink(&target, &linkpath)
1231 .map(|_| CopyStats::default())
1232 .map_err(FxcpError::Io)
1233 }).await.map_err(FxcpError::Join)?
1234 }
1235 Err(e) => Err(e),
1236 }
1237 },
1238 }
1239 }
1240
1241 fn probe_sparse_with_segments(src_fd: RawFd, file_size: u64) -> Option<(f64, Vec<FileSegment>)> {
1246 if file_size == 0 {
1247 return None;
1248 }
1249 let mut segments: Vec<FileSegment> = Vec::new();
1250 let mut hole_bytes: u64 = 0;
1251 let mut current_offset: i64 = 0;
1252 let end_offset = file_size as i64;
1253
1254 loop {
1255 let data_offset_res = unsafe { libc::lseek(src_fd, current_offset, libc::SEEK_DATA) };
1257 if data_offset_res < 0 {
1258 let err = std::io::Error::last_os_error();
1259 match err.raw_os_error() {
1260 Some(libc::ENXIO) => {
1261 let hole_len = (end_offset - current_offset) as u64;
1263 if hole_len > 0 {
1264 hole_bytes += hole_len;
1265 segments.push(FileSegment::Hole { offset: current_offset as u64, len: hole_len });
1266 }
1267 break;
1268 }
1269 Some(libc::EOPNOTSUPP) | Some(libc::EINVAL) => return None,
1270 _ => break,
1271 }
1272 }
1273 let data_offset = data_offset_res.min(end_offset);
1274 if data_offset > current_offset {
1275 let hole_len = (data_offset - current_offset) as u64;
1276 hole_bytes += hole_len;
1277 segments.push(FileSegment::Hole { offset: current_offset as u64, len: hole_len });
1278 current_offset = data_offset;
1279 }
1280 if current_offset >= end_offset {
1281 break;
1282 }
1283
1284 let hole_offset_res = unsafe { libc::lseek(src_fd, current_offset, libc::SEEK_HOLE) };
1286 let hole_offset = if hole_offset_res < 0 {
1287 end_offset
1288 } else {
1289 hole_offset_res.min(end_offset)
1290 };
1291
1292 let data_len = (hole_offset - current_offset) as u64;
1293 if data_len > 0 {
1294 segments.push(FileSegment::Data { offset: current_offset as u64, len: data_len });
1295 }
1296 current_offset = hole_offset;
1297 if current_offset >= end_offset {
1298 break;
1299 }
1300 }
1301
1302 unsafe { libc::lseek(src_fd, 0, libc::SEEK_SET) };
1304
1305 let ratio = hole_bytes as f64 / file_size as f64;
1306 Some((ratio, segments))
1307 }
1308
1309 fn prepare_destination_file(
1310 path: PathBuf,
1311 direct_io: bool,
1312 f2fs_atomic: bool,
1313 size: u64,
1314 is_sparse: bool,
1315 src_fd: Option<RawFd>,
1316 is_nfs: bool,
1317 ) -> Result<(std::fs::File, RawFd, Option<Vec<FileSegment>>)> {
1318 debug!("prepare_destination_file: {:?}", path);
1319 prepare_target_parent(&path)?;
1320 let mut open_opts = std::fs::OpenOptions::new();
1321 open_opts.read(true).write(true).create(true).truncate(true);
1322 use std::os::unix::fs::OpenOptionsExt;
1323 open_opts.custom_flags(libc::O_NOFOLLOW);
1324 if direct_io {
1325 open_opts.custom_flags(libc::O_DIRECT | libc::O_NOFOLLOW);
1326 }
1327
1328 let file = match open_opts.open(&path) {
1329 Ok(f) => f,
1330 Err(e) => {
1331 error!("prepare_destination_file: Failed to open {:?}: {}", path, e);
1332 return Err(FxcpError::Io(e));
1333 }
1334 };
1335 let fd = file.as_raw_fd();
1336
1337 if f2fs_atomic {
1338 Self::set_f2fs_pinning(fd, true);
1339 if unsafe { libc::ioctl(fd, F2FS_IOC_START_ATOMIC_WRITE) } < 0 {
1341 warn!("F2FS: Failed to start atomic write transaction. Proceeding non-atomically.");
1342 }
1343 }
1344
1345 let cached_segments = if is_sparse || is_nfs {
1348 None
1349 } else {
1350 src_fd.and_then(|fd| Self::probe_sparse_with_segments(fd, size))
1351 };
1352 let skip_fallocate = is_sparse || is_nfs
1356 || cached_segments.as_ref().is_some_and(|(ratio, _)| *ratio > crate::constants::SPARSE_HOLE_RATIO_SKIP_THRESHOLD);
1357 let cached_segment_vec = cached_segments.map(|(_, segs)| segs);
1358
1359 if skip_fallocate && size > 0 {
1360 debug!("prepare_destination_file: Skipping pre-allocation (sparse or high hole ratio, {} bytes)", size);
1361 } else if size > 0 {
1362 debug!("prepare_destination_file: Fallocating {} bytes for {:?}", size, path);
1363 let ret = unsafe { libc::fallocate(fd, 0, 0, size as i64) };
1366 if ret < 0 {
1367 let err = std::io::Error::last_os_error();
1368 if err.raw_os_error() != Some(libc::EOPNOTSUPP) {
1369 warn!("Prepare: fallocate failed: {}. Performance may degrade.", err);
1370 }
1371 }
1372 }
1373 debug!("prepare_destination_file: Ready {:?}", path);
1374 Ok((file, fd, cached_segment_vec))
1375 }
1376
1377 async fn submit_and_wait_single(
1381 ring: &mut IoUring,
1382 sqe: io_uring::squeue::Entry,
1383 async_fd: &AsyncFd<RawFd>,
1384 ) -> Result<i32> {
1385 unsafe { ring.submission().push(&sqe) }
1387 .map_err(|_| FxcpError::Io(io::Error::other("io_uring submission queue full")))?;
1388
1389 ring.submit().map_err(FxcpError::Io)?;
1390
1391 loop {
1392 if let Some(cqe) = ring.completion().next() {
1393 let res = cqe.result();
1394 if res < 0 {
1395 return Err(FxcpError::Io(io::Error::from_raw_os_error(-res)));
1396 }
1397 return Ok(res);
1398 }
1399
1400 match tokio::time::timeout(
1401 Duration::from_millis(crate::constants::IO_URING_READABLE_TIMEOUT_MS),
1402 async_fd.readable()
1403 ).await {
1404 Ok(Ok(mut guard)) => {
1405 let mut buf = [0u8; 8];
1406 let _ = unsafe { libc::read(async_fd.get_ref().as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, 8) };
1408 guard.clear_ready();
1409 }
1410 Ok(Err(e)) => return Err(FxcpError::Io(e)),
1411 Err(_) => {
1412 let _ = ring.submit();
1414 }
1415 }
1416 }
1417 }
1418
1419 async fn execute_copy_file(
1420 src: &Path,
1421 dst: &Path,
1422 ring: &mut IoUring,
1423 buffer_pool: &mut BufferPool,
1424 atomic_pool: Option<&mut BufferPool>,
1425 async_fd: Arc<AsyncFd<RawFd>>,
1426 vdo_opt: bool,
1427 direct_io_ok: bool,
1428 source_caps: &Arc<Capabilities>,
1429 target_caps: &Arc<Capabilities>,
1430 src_file_size: u64,
1431 vdo_stall_threshold: u32,
1432 barrier_callback: &Option<Box<dyn Fn(u64) + Send + Sync>>,
1433 source_uncached: bool,
1434 target_uncached: bool,
1435 governor: Option<Arc<Governor>>,
1436 target_label: String,
1437 fsync_tracker: &mut FsyncLatencyTracker,
1438 buffer_limit: Option<usize>,
1439 skip_fsync: bool,
1440 segment_stall_timeout_secs: u64,
1441 segment_overall_timeout_secs: u64,
1442 is_sparse: bool,
1443 postcopy_ring: &mut IoUring,
1444 postcopy_async_fd: &Arc<AsyncFd<RawFd>>,
1445 ) -> Result<CopyStats> {
1446 let target_path = dst.with_extension(format!("tmp.{}", Uuid::new_v4()));
1447 debug!("execute_copy_file: {:?} -> {:?}", src, target_path);
1448
1449 let res = Self::execute_full_copy_logic(
1450 src, &target_path, ring, buffer_pool, atomic_pool, async_fd, vdo_opt, direct_io_ok,
1451 source_caps, target_caps, false, src_file_size, vdo_stall_threshold,
1452 barrier_callback, source_uncached, target_uncached, governor,
1453 target_label, fsync_tracker, buffer_limit, skip_fsync,
1454 segment_stall_timeout_secs, segment_overall_timeout_secs,
1455 is_sparse,
1456 postcopy_ring, postcopy_async_fd,
1457 ).await?;
1458
1459 match res {
1460 Ok(stats) => {
1461
1462 let can_exchange = target_caps.exchange_range.load(Ordering::Relaxed);
1465 let dst_exists = dst.exists();
1466
1467 if can_exchange && dst_exists {
1468 let tp2 = target_path.clone();
1469 let dst_owned = dst.to_path_buf();
1470 let target_caps_clone = target_caps.clone();
1471
1472 spawn_blocking(move || {
1473 let is_btrfs = if let Ok(s) = statfs::statfs(&tp2) {
1474 s.filesystem_type().0 as u64 == BTRFS_SUPER_MAGIC
1475 } else {
1476 false
1477 };
1478
1479 if is_btrfs {
1480 crate::consistency::atomic_rename(&tp2, &dst_owned, Some(libc::RENAME_EXCHANGE))
1481 .or_else(|_| crate::consistency::atomic_rename(&tp2, &dst_owned, None))
1482 .map_err(FxcpError::Io)
1483 } else {
1484 match crate::consistency::exchange::atomic_exchange(&tp2, &dst_owned) {
1485 Ok(_) => Ok(()),
1486 Err(e) => {
1487 let raw_err = e.raw_os_error();
1488 if raw_err == Some(libc::EOPNOTSUPP) || raw_err == Some(libc::ENOTTY) {
1489 debug!("Atomic Exchange unsupported on target ({}). Disabling optimization.", e);
1490 target_caps_clone.exchange_range.store(false, Ordering::Relaxed);
1491 } else {
1492 warn!("Atomic Exchange failed: {}. Falling back to rename.", e);
1493 }
1494 crate::consistency::atomic_rename(&tp2, &dst_owned, None).map_err(FxcpError::Io)
1495 }
1496 }
1497 }
1498 }).await.map_err(FxcpError::Join)??;
1499 } else {
1500 let old_cstr = CString::new(target_path.as_os_str().as_bytes())
1501 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
1502 let new_cstr = CString::new(dst.as_os_str().as_bytes())
1503 .map_err(|_| FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte")))?;
1504 let rename_sqe = opcode::RenameAt::new(
1505 types::Fd(libc::AT_FDCWD), old_cstr.as_ptr(),
1506 types::Fd(libc::AT_FDCWD), new_cstr.as_ptr(),
1507 ).build().user_data(POSTCOPY_RENAME_OP);
1508 match Self::submit_and_wait_single(postcopy_ring, rename_sqe, postcopy_async_fd).await {
1509 Ok(_) => {},
1510 Err(FxcpError::Io(ref e)) if e.raw_os_error() == Some(libc::EBADF)
1511 || e.raw_os_error() == Some(libc::EOPNOTSUPP) => {
1512 debug!("io_uring RenameAt failed ({}), falling back to blocking rename", e);
1513 let tp_clone = target_path.clone();
1514 let dst_clone = dst.to_path_buf();
1515 spawn_blocking(move || {
1516 crate::consistency::atomic_rename(&tp_clone, &dst_clone, None)
1517 .map_err(FxcpError::Io)
1518 }).await.map_err(FxcpError::Join)??;
1519 }
1520 Err(e) => return Err(e),
1521 }
1522 }
1523
1524 if let Some(cb) = barrier_callback
1525 && let Ok(meta) = std::fs::metadata(dst) { cb(meta.ino()); }
1526 debug!("execute_copy_file: Success {:?}", dst);
1527 Ok(stats)
1528 },
1529 Err(e) => {
1530 error!("execute_copy_file: Failed {:?} - {}", target_path, e);
1531 let _ = std::fs::remove_file(&target_path);
1532 Err(e)
1533 }
1534 }
1535 }
1536
1537 async fn execute_copy_range(
1538 src: &Path,
1539 dst: &Path,
1540 ring: &mut IoUring,
1541 buffer_pool: &mut BufferPool,
1542 atomic_pool: Option<&mut BufferPool>,
1543 async_fd: Arc<AsyncFd<RawFd>>,
1544 vdo_opt: bool,
1545 offset: u64,
1546 length: u64,
1547 direct_io_ok: bool,
1548 _src_file_size: u64,
1549 source_caps: &Arc<Capabilities>,
1550 target_caps: &Arc<Capabilities>,
1551 vdo_stall_threshold: u32,
1552 source_uncached: bool,
1553 target_uncached: bool,
1554 governor: Option<Arc<Governor>>,
1555 target_label: String,
1556 fsync_tracker: &mut FsyncLatencyTracker,
1557 buffer_limit: Option<usize>,
1558 skip_fsync: bool,
1559 segment_stall_timeout_secs: u64,
1560 segment_overall_timeout_secs: u64,
1561 ) -> Result<CopyStats> {
1562 let src_meta = std::fs::metadata(src).map_err(FxcpError::Io)?;
1563 let strategy = determine_copy_strategy(src, dst, &src_meta, target_caps);
1564
1565 let sf = Self::open_source_noatime(src).await?;
1566 let sfd = sf.as_raw_fd();
1567
1568 let mut open_opts = tokio::fs::OpenOptions::new();
1569 open_opts.read(true).write(true).create(false);
1570
1571 let mut use_direct_io = direct_io_ok;
1572 if use_direct_io && (!length.is_multiple_of(crate::constants::MINIMUM_ALIGNMENT_BYTES as u64) || !offset.is_multiple_of(crate::constants::MINIMUM_ALIGNMENT_BYTES as u64)) {
1573 use_direct_io = false;
1574 }
1575 if use_direct_io { open_opts.custom_flags(libc::O_DIRECT); }
1576
1577 let df = open_opts.open(dst).await.map_err(FxcpError::Io)?;
1578 let dfd = df.as_raw_fd();
1579
1580 let use_fixed = ring.submitter().register_files(&[sfd, dfd]).is_ok();
1581 if !use_fixed {
1582 warn!("io_uring: register_files failed in execute_copy_range, falling back to non-fixed opcodes");
1583 }
1584
1585 let mut reflink_done = false;
1586 let mut stats = CopyStats::default();
1587
1588 if strategy == CopyStrategy::Reflink {
1589 let reflink_res = Self::try_reflink_range(sfd, dfd, offset, length, offset, target_label.clone()).await;
1590 if let Ok(s) = reflink_res { stats = s; reflink_done = true; }
1591 }
1592
1593 let target_is_nfs = target_caps.is_nfs.load(Ordering::Relaxed);
1594
1595 let res = if reflink_done { Ok(stats) } else if !target_is_nfs {
1596 debug!("io_uring: register_files failed on non-NFS target in copy_range, using copy_file_range/sendfile directly");
1599 let range_offset = offset;
1600 let range_len = length;
1601 let tl = target_label.clone();
1602 let fallback_res = spawn_blocking(move || {
1603 let start = Instant::now();
1604 let mut off_in = range_offset as i64;
1605 let mut off_out = range_offset as i64;
1606 let mut remaining = range_len;
1607 while remaining > 0 {
1608 let chunk = remaining.min(crate::constants::COPY_FILE_RANGE_CHUNK_SIZE) as usize;
1609 let ret = unsafe {
1611 libc::copy_file_range(sfd, &mut off_in, dfd, &mut off_out, chunk, 0)
1612 };
1613 if ret < 0 {
1614 let err = std::io::Error::last_os_error();
1615 if err.raw_os_error() == Some(libc::EXDEV) || err.raw_os_error() == Some(libc::EOPNOTSUPP) {
1616 break;
1617 }
1618 return Err(FxcpError::Io(err));
1619 }
1620 if ret == 0 { break; }
1621 remaining -= ret as u64;
1622 }
1623 if remaining == 0 {
1624 return Ok(CopyStats {
1625 bytes_processed: range_len,
1626 bytes_zeros: 0,
1627 io_duration: start.elapsed(),
1628 ops_count: 1,
1629 });
1630 }
1631 unsafe { libc::lseek(sfd, range_offset as i64, libc::SEEK_SET) };
1633 unsafe { libc::lseek(dfd, range_offset as i64, libc::SEEK_SET) };
1634 let mut total: u64 = 0;
1635 let target = range_len - (range_len - remaining);
1636 while total < target {
1637 let rem = target - total;
1638 let ret = unsafe {
1640 libc::sendfile(dfd, sfd, std::ptr::null_mut(), rem as usize)
1641 };
1642 if ret < 0 { return Err(FxcpError::Io(std::io::Error::last_os_error())); }
1643 if ret == 0 { break; }
1644 total += ret as u64;
1645 }
1646 Ok(CopyStats {
1647 bytes_processed: range_len,
1648 bytes_zeros: 0,
1649 io_duration: start.elapsed(),
1650 ops_count: 1,
1651 })
1652 }).await.map_err(FxcpError::Join)?;
1653 match fallback_res {
1654 Ok(sf_stats) => {
1655 metrics::COPY_METHOD_SENDFILE_FALLBACK.with_label_values(&[&tl]).inc();
1656 Ok(sf_stats)
1657 }
1658 Err(e) => Err(e),
1659 }
1660 } else {
1661 let target_label_uring = target_label.clone();
1662 let uring_res = Self::perform_delta_uring_pipelined(
1663 ring, sfd, dfd, offset, length, vdo_opt, buffer_pool, atomic_pool, async_fd,
1664 dst.to_path_buf(), source_caps, target_caps, false, vdo_stall_threshold,
1665 source_uncached, target_uncached, governor, target_label_uring, fsync_tracker, buffer_limit, skip_fsync,
1666 segment_stall_timeout_secs, segment_overall_timeout_secs,
1667 use_fixed, None,
1668 ).await;
1669
1670 match uring_res {
1671 Ok(s) => Ok(s),
1672 Err(uring_err) => {
1673 let is_recoverable = matches!(&uring_err, FxcpError::Io(e)
1674 if e.raw_os_error() == Some(libc::EBADF)
1675 || e.raw_os_error() == Some(libc::EFAULT)
1676 || e.kind() == io::ErrorKind::TimedOut);
1677
1678 if is_recoverable {
1679 warn!("io_uring range copy failed ({}), falling back to copy_file_range/sendfile", uring_err);
1680 let range_offset = offset;
1681 let range_len = length;
1682 let tl = target_label.clone();
1683 let sendfile_res = spawn_blocking(move || {
1684 let start = Instant::now();
1685 let mut off_in = range_offset as i64;
1686 let mut off_out = range_offset as i64;
1687 let mut remaining = range_len;
1688 while remaining > 0 {
1689 let chunk = remaining.min(crate::constants::COPY_FILE_RANGE_CHUNK_SIZE) as usize;
1690 let ret = unsafe {
1692 libc::copy_file_range(sfd, &mut off_in, dfd, &mut off_out, chunk, 0)
1693 };
1694 if ret < 0 {
1695 let err = std::io::Error::last_os_error();
1696 if err.raw_os_error() == Some(libc::EXDEV) || err.raw_os_error() == Some(libc::EOPNOTSUPP) {
1697 break;
1698 }
1699 return Err(FxcpError::Io(err));
1700 }
1701 if ret == 0 { break; }
1702 remaining -= ret as u64;
1703 }
1704 if remaining == 0 {
1705 return Ok(CopyStats {
1706 bytes_processed: range_len,
1707 bytes_zeros: 0,
1708 io_duration: start.elapsed(),
1709 ops_count: 1,
1710 });
1711 }
1712 unsafe { libc::lseek(sfd, range_offset as i64, libc::SEEK_SET) };
1714 unsafe { libc::lseek(dfd, range_offset as i64, libc::SEEK_SET) };
1715 let mut total: u64 = 0;
1716 let target = range_len - (range_len - remaining);
1717 while total < target {
1718 let rem = target - total;
1719 let ret = unsafe {
1721 libc::sendfile(dfd, sfd, std::ptr::null_mut(), rem as usize)
1722 };
1723 if ret < 0 { return Err(FxcpError::Io(std::io::Error::last_os_error())); }
1724 if ret == 0 { break; }
1725 total += ret as u64;
1726 }
1727 Ok(CopyStats {
1728 bytes_processed: range_len,
1729 bytes_zeros: 0,
1730 io_duration: start.elapsed(),
1731 ops_count: 1,
1732 })
1733 }).await.map_err(FxcpError::Join)?;
1734
1735 match sendfile_res {
1736 Ok(sf_stats) => {
1737 metrics::COPY_METHOD_SENDFILE_FALLBACK.with_label_values(&[&tl]).inc();
1738 Ok(sf_stats)
1739 }
1740 Err(_) => Err(uring_err),
1741 }
1742 } else {
1743 Err(uring_err)
1744 }
1745 }
1746 }
1747 };
1748
1749 if use_fixed { let _ = ring.submitter().unregister_files(); }
1750 drop(sf); drop(df);
1751 res
1752 }
1753
1754 async fn execute_full_copy_logic(
1755 src: &Path,
1756 target_path: &Path,
1757 ring: &mut IoUring,
1758 buffer_pool: &mut BufferPool,
1759 atomic_pool: Option<&mut BufferPool>,
1760 async_fd: Arc<AsyncFd<RawFd>>,
1761 vdo_opt: bool,
1762 direct_io_ok: bool,
1763 source_caps: &Arc<Capabilities>,
1764 target_caps: &Arc<Capabilities>,
1765 use_atomic: bool,
1766 src_file_size: u64,
1767 vdo_stall_threshold: u32,
1768 _barrier_callback: &Option<Box<dyn Fn(u64) + Send + Sync>>,
1769 source_uncached: bool,
1770 target_uncached: bool,
1771 governor: Option<Arc<Governor>>,
1772 target_label: String,
1773 fsync_tracker: &mut FsyncLatencyTracker,
1774 buffer_limit: Option<usize>,
1775 skip_fsync: bool,
1776 segment_stall_timeout_secs: u64,
1777 segment_overall_timeout_secs: u64,
1778 is_sparse: bool,
1779 postcopy_ring: &mut IoUring,
1780 postcopy_async_fd: &Arc<AsyncFd<RawFd>>,
1781 ) -> Result<std::result::Result<CopyStats, FxcpError>> {
1782 debug!("execute_full_copy_logic: Start {:?} -> {:?}", src, target_path);
1783
1784 if !src.exists() {
1785 return Err(FxcpError::Io(std::io::Error::new(
1786 std::io::ErrorKind::NotFound,
1787 format!("Source file no longer exists: {:?}", src)
1788 )));
1789 }
1790
1791 let sf = Self::open_source_noatime(src).await?;
1792 let sfd = sf.as_raw_fd();
1793
1794 let mut use_direct_io = direct_io_ok;
1795 if use_direct_io && !src_file_size.is_multiple_of(crate::constants::MINIMUM_ALIGNMENT_BYTES as u64) {
1796 use_direct_io = false;
1797 }
1798
1799 let use_f2fs = target_caps.f2fs_atomic_legacy.load(Ordering::Relaxed);
1800 let path_clone = target_path.to_path_buf();
1801
1802 let target_is_nfs = target_caps.is_nfs.load(Ordering::Relaxed);
1803
1804 debug!("execute_full_copy_logic: Preparing destination...");
1805 let (_file_handle, dfd, cached_segments) = spawn_blocking(move || {
1806 Self::prepare_destination_file(path_clone, use_direct_io, use_f2fs, src_file_size, is_sparse, Some(sfd), target_is_nfs)
1807 }).await.map_err(FxcpError::Join)??;
1808
1809 let use_fixed = ring.submitter().register_files(&[sfd, dfd]).is_ok();
1810 if use_fixed {
1811 debug!("execute_full_copy_logic: Registered fixed files (src={}, dst={})", sfd, dfd);
1812 } else {
1813 warn!("io_uring: register_files failed, falling back to non-fixed opcodes");
1814 }
1815
1816 let mut transfer_done = false;
1817 let mut stats = CopyStats::default();
1818 let src_meta = std::fs::metadata(src).map_err(FxcpError::Io)?;
1819 let strategy = determine_copy_strategy(src, target_path, &src_meta, target_caps);
1820
1821 if !use_atomic && !use_f2fs && !target_is_nfs && strategy == CopyStrategy::Reflink {
1822 debug!("execute_full_copy_logic: Attempting reflink...");
1823 let src_path_owned = src.to_path_buf();
1824 let reflink_res = Self::try_reflink(sfd, dfd, src_file_size, src_path_owned, target_label.clone()).await;
1825 if let Ok(reflink_stats) = reflink_res {
1826 stats = reflink_stats;
1827 transfer_done = true;
1828 debug!("execute_full_copy_logic: Reflink success");
1829 }
1830 else {
1831 debug!("execute_full_copy_logic: Reflink failed/partial, falling back. Error: {:?}", reflink_res.err());
1832 unsafe { libc::lseek(dfd, 0, libc::SEEK_SET); libc::lseek(sfd, 0, libc::SEEK_SET); };
1835 }
1836 }
1837
1838 let mut result_val = Ok(stats);
1839
1840 if !transfer_done
1841 && let Some(sf_stats) = try_nfs_sendfile(sfd, dfd, src_file_size, is_sparse, target_is_nfs, &target_label).await? {
1842 result_val = Ok(sf_stats);
1843 transfer_done = true;
1844 }
1845
1846 if !transfer_done
1847 && let Some(sf_stats) = try_cachestat_sendfile(sfd, dfd, src_file_size, is_sparse, &target_label).await? {
1848 result_val = Ok(sf_stats);
1849 transfer_done = true;
1850 }
1851
1852 if !transfer_done && !target_is_nfs && !is_sparse {
1857 debug!("Local non-sparse target: using sendfile (skipping io_uring)");
1858 let sf_size = src_file_size;
1859 let tl = target_label.clone();
1860 let sendfile_res = spawn_blocking(move || {
1861 sendfile_full(sfd, dfd, sf_size, "sendfile local")
1862 }).await.map_err(FxcpError::Join)?;
1863 match sendfile_res {
1864 Ok(sf_stats) => {
1865 metrics::COPY_METHOD_SENDFILE_FALLBACK.with_label_values(&[&tl]).inc();
1866 result_val = Ok(sf_stats);
1867 transfer_done = true;
1868 }
1869 Err(e) => { result_val = Err(e); }
1870 }
1871 }
1872
1873 if !transfer_done {
1874 debug!("execute_full_copy_logic: Starting io_uring pipeline...");
1875 let target_label_uring = target_label.clone();
1876 let uring_res = Self::perform_delta_uring_pipelined(
1877 ring, sfd, dfd, 0, src_file_size, vdo_opt, buffer_pool, atomic_pool, async_fd,
1878 target_path.to_path_buf(), source_caps, target_caps, use_atomic,
1879 vdo_stall_threshold, source_uncached, target_uncached, governor,
1880 target_label_uring, fsync_tracker, buffer_limit, skip_fsync,
1881 segment_stall_timeout_secs, segment_overall_timeout_secs,
1882 use_fixed, cached_segments,
1883 ).await;
1884
1885 match uring_res {
1886 Ok(stats) => { result_val = Ok(stats); }
1887 Err(uring_err) => {
1888 let is_recoverable = matches!(&uring_err, FxcpError::Io(e)
1889 if e.raw_os_error() == Some(libc::EBADF)
1890 || e.raw_os_error() == Some(libc::EFAULT)
1891 || e.kind() == io::ErrorKind::TimedOut);
1892
1893 if is_recoverable {
1894 warn!("io_uring pipeline failed ({}), falling back to sendfile", uring_err);
1895 unsafe {
1899 libc::lseek(sfd, 0, libc::SEEK_SET);
1900 libc::ftruncate(dfd, 0);
1901 libc::lseek(dfd, 0, libc::SEEK_SET);
1902 };
1903 if src_file_size > 0 && !is_sparse {
1904 let _ = unsafe { libc::fallocate(dfd, 0, 0, src_file_size as i64) };
1906 }
1907 let sf_size = src_file_size;
1908 let tl = target_label.clone();
1909 let sendfile_res = spawn_blocking(move || {
1910 sendfile_full(sfd, dfd, sf_size, "sendfile fallback")
1911 }).await.map_err(FxcpError::Join)?;
1912
1913 match sendfile_res {
1914 Ok(sf_stats) => {
1915 if is_sparse {
1916 warn!("Sparse file copied via sendfile fallback -- holes not preserved");
1917 }
1918 metrics::COPY_METHOD_SENDFILE_FALLBACK.with_label_values(&[&tl]).inc();
1919 result_val = Ok(sf_stats);
1920 }
1921 Err(sf_err) => {
1922 error!("sendfile fallback also failed: {}", sf_err);
1923 result_val = Err(uring_err);
1924 }
1925 }
1926 } else {
1927 result_val = Err(uring_err);
1928 }
1929 }
1930 }
1931 }
1932
1933 if use_f2fs {
1934 let should_commit = result_val.is_ok();
1935 let commit_res = spawn_blocking(move || {
1936 let res = if should_commit {
1937 if unsafe { libc::ioctl(dfd, F2FS_IOC_COMMIT_ATOMIC_WRITE) } < 0 {
1939 error!("F2FS: Atomic Commit Failed!");
1940 Err(FxcpError::Io(std::io::Error::last_os_error()))
1941 } else {
1942 Ok(())
1943 }
1944 } else {
1945 let _ = unsafe { libc::ioctl(dfd, F2FS_IOC_ABORT_ATOMIC_WRITE) };
1947 Ok(())
1948 };
1949 Self::set_f2fs_pinning(dfd, false);
1950 res
1951 }).await.map_err(FxcpError::Join)?;
1952
1953 if let Err(e) = commit_res
1954 && result_val.is_ok() { result_val = Err(e); }
1955 }
1956
1957 if use_fixed {
1958 let _ = ring.submitter().unregister_files();
1959 }
1960
1961 if !skip_fsync
1964 && let Ok(ref _stats) = result_val {
1965 let fsync_sqe = opcode::Fsync::new(types::Fd(dfd))
1966 .build()
1967 .user_data(POSTCOPY_FSYNC_OP);
1968 match Self::submit_and_wait_single(postcopy_ring, fsync_sqe, postcopy_async_fd).await {
1969 Ok(_) => {},
1970 Err(FxcpError::Io(ref e)) if e.raw_os_error() == Some(libc::EBADF)
1971 || e.raw_os_error() == Some(libc::EFAULT) => {
1972 debug!("Post-copy io_uring fsync failed ({}), falling back to blocking fsync", e);
1973 unsafe { libc::fsync(dfd) };
1975 }
1976 Err(e) => return Ok(Err(e)),
1977 }
1978 }
1979
1980 {
1983 let fadv_sqe = opcode::Fadvise::new(types::Fd(sfd), 0, libc::POSIX_FADV_DONTNEED)
1984 .build()
1985 .user_data(FADVISE_OP);
1986 if unsafe { ring.submission().push(&fadv_sqe) }.is_ok() {
1988 let _ = ring.submit();
1989 }
1990 }
1992
1993 drop(sf);
1994 debug!("execute_full_copy_logic: Finished");
1995 Ok(result_val)
1996 }
1997
1998 async fn try_reflink(sfd: i32, dfd: i32, src_file_size: u64, src_path: PathBuf, target_label: String) -> Result<CopyStats> {
1999 spawn_blocking(move || {
2000 let start = Instant::now();
2001 let ret = unsafe { libc::ioctl(dfd, FICLONE, sfd) };
2003 if ret == 0 {
2004 metrics::COPY_METHOD_REFLINK.with_label_values(&[&target_label]).inc();
2005 return Ok(CopyStats { bytes_processed: src_file_size, bytes_zeros: 0, io_duration: start.elapsed(), ops_count: 1 });
2006 }
2007 let err = std::io::Error::last_os_error();
2008 if err.raw_os_error() != Some(libc::EOPNOTSUPP) && err.raw_os_error() != Some(libc::EXDEV) {
2009 warn!("Reflink failed on supported target ({}). Fallback copy active. Path: {:?}", err, src_path);
2010 } else {
2011 debug!("Reflink not supported or cross-device ({}), falling back.", err);
2012 }
2013
2014 let mut total_reflinked = 0usize;
2015 let size_usize: usize = src_file_size.try_into().unwrap_or(usize::MAX);
2016 let mut ops = 0;
2017
2018 if src_file_size > 0 {
2019 let mut off_in = 0i64;
2020 let mut off_out = 0i64;
2021 while total_reflinked < size_usize {
2022 let remaining = size_usize - total_reflinked;
2023 let chunk = std::cmp::min(remaining, crate::constants::COPY_FILE_RANGE_CHUNK_SIZE as usize);
2024 let ret = unsafe { libc::copy_file_range(sfd, &mut off_in, dfd, &mut off_out, chunk, 0) };
2027 if ret < 0 {
2028 let os_err = std::io::Error::last_os_error();
2029 return Err(FxcpError::Io(io::Error::new(
2030 os_err.kind(),
2031 format!("copy_file_range failed for {}: {}", target_label, os_err),
2032 )));
2033 } else if ret == 0 {
2034 break;
2035 }
2036 total_reflinked += ret as usize;
2037 ops += 1;
2038 }
2039 }
2040
2041 if total_reflinked == size_usize {
2042 let is_network_fs = match statfs::statfs(&src_path) {
2043 Ok(s) => { let magic = s.filesystem_type().0 as u64; magic == NFS_SUPER_MAGIC || magic == 0x517B || magic == 0xFF534D42 },
2044 Err(_) => false,
2045 };
2046 if is_network_fs { metrics::COPY_METHOD_OFFLOAD.with_label_values(&[&target_label]).inc(); } else { metrics::COPY_METHOD_REFLINK.with_label_values(&[&target_label]).inc(); }
2047 Ok(CopyStats { bytes_processed: src_file_size, bytes_zeros: 0, io_duration: start.elapsed(), ops_count: ops })
2048 } else {
2049 Err(FxcpError::Io(io::Error::other("Reflink partial copy or EOF")))
2050 }
2051 }).await.map_err(FxcpError::Join)?
2052 }
2053
2054 async fn try_reflink_range(sfd: i32, dfd: i32, src_offset: u64, len: u64, dst_offset: u64, target_label: String) -> Result<CopyStats> {
2055 spawn_blocking(move || {
2056 let start = Instant::now();
2057 let args = FileCloneRange { src_fd: sfd as i64, src_offset, src_length: len, dest_offset: dst_offset };
2058 let ret = unsafe { libc::ioctl(dfd, FICLONERANGE, &args) };
2061 if ret == 0 {
2062 metrics::COPY_METHOD_REFLINK.with_label_values(&[&target_label]).inc();
2063 Ok(CopyStats { bytes_processed: len, bytes_zeros: 0, io_duration: start.elapsed(), ops_count: 1 })
2064 } else {
2065 let err = std::io::Error::last_os_error();
2066 if err.raw_os_error() != Some(libc::EOPNOTSUPP) && err.raw_os_error() != Some(libc::EXDEV) {
2067 warn!("Reflink range failed on supported target ({}). Fallback copy active.", err);
2068 }
2069 Err(FxcpError::Io(io::Error::new(
2070 err.kind(),
2071 format!("FICLONERANGE ioctl failed for {}: {}", target_label, err),
2072 )))
2073 }
2074 }).await.map_err(FxcpError::Join)?
2075 }
2076
2077 fn check_atomic_invariants(caps: &Arc<Capabilities>, offset: u64, length: u32, buffer_addr: usize) -> Result<()> {
2078 let min = caps.atomic_min_bytes.load(Ordering::Relaxed) as u64;
2079 let max = caps.atomic_max_bytes.load(Ordering::Relaxed) as u64;
2080
2081 if min == 0 || max == 0 {
2082 return Err(FxcpError::Io(io::Error::new(io::ErrorKind::Unsupported, "Atomic writes not supported by hardware")));
2083 }
2084
2085 let len_u64 = length as u64;
2086 if !len_u64.is_power_of_two() {
2087 return Err(FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, format!("Atomic write length {} is not power of 2", len_u64))));
2088 }
2089
2090 if len_u64 < min || len_u64 > max {
2091 return Err(FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, format!("Atomic write length {} out of bounds ({}-{})", len_u64, min, max))));
2092 }
2093
2094 if !offset.is_multiple_of(len_u64) {
2095 return Err(FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, format!("Atomic write offset {} not aligned to length {}", offset, len_u64))));
2096 }
2097
2098 if !(buffer_addr as u64).is_multiple_of(len_u64) {
2099 return Err(FxcpError::Io(io::Error::new(io::ErrorKind::InvalidInput, "Buffer memory not aligned for atomic write")));
2100 }
2101
2102 Ok(())
2103 }
2104
2105 fn map_sparse_segments(
2106 sfd: RawFd,
2107 offset: u64,
2108 length: u64
2109 ) -> Result<Vec<FileSegment>> {
2110 trace!("map_sparse_segments: sfd={}, offset={}, len={}", sfd, offset, length);
2111 let mut segments = Vec::new();
2112 let mut current_offset = offset;
2113 let end_offset = offset + length;
2114
2115 while current_offset < end_offset {
2116 let data_offset_res = unsafe { libc::lseek(sfd, current_offset as i64, libc::SEEK_DATA) };
2118 if data_offset_res < 0 {
2119 let err = std::io::Error::last_os_error();
2120 match err.raw_os_error() {
2121 Some(libc::ENXIO) => {
2122 let hole_len = end_offset - current_offset;
2123 if hole_len > 0 {
2124 segments.push(FileSegment::Hole { offset: current_offset, len: hole_len });
2125 }
2126 break;
2127 },
2128 _ => return Err(FxcpError::Io(err)),
2129 }
2130 }
2131 let data_offset = data_offset_res as u64;
2132
2133 if data_offset >= end_offset {
2134 let hole_len = end_offset - current_offset;
2135 if hole_len > 0 {
2136 segments.push(FileSegment::Hole { offset: current_offset, len: hole_len });
2137 }
2138 break;
2139 }
2140
2141 if data_offset > current_offset {
2142 let hole_len = data_offset - current_offset;
2143 segments.push(FileSegment::Hole { offset: current_offset, len: hole_len });
2144 current_offset = data_offset;
2145 }
2146
2147 let hole_offset_res = unsafe { libc::lseek(sfd, current_offset as i64, libc::SEEK_HOLE) };
2149 let hole_offset = if hole_offset_res < 0 {
2150 end_offset
2151 } else {
2152 hole_offset_res as u64
2153 };
2154
2155 let segment_end = hole_offset.min(end_offset);
2156 let segment_len = segment_end - current_offset;
2157
2158 if segment_len > 0 {
2159 segments.push(FileSegment::Data { offset: current_offset, len: segment_len });
2160 current_offset += segment_len;
2161 } else {
2162 warn!("map_sparse_segments: Infinite loop guard triggered. off={} len={}", current_offset, segment_len);
2163 break;
2164 }
2165 }
2166 trace!("map_sparse_segments: found {} segments", segments.len());
2167 Ok(segments)
2168 }
2169
2170 async fn perform_delta_uring_pipelined(
2171 ring: &mut IoUring,
2172 sfd: i32,
2173 dfd: i32,
2174 offset: u64,
2175 length: u64,
2176 vdo_opt: bool,
2177 buffer_pool: &mut BufferPool,
2178 mut atomic_pool: Option<&mut BufferPool>,
2179 async_fd: Arc<AsyncFd<RawFd>>,
2180 _debug_path: PathBuf,
2181 source_caps: &Arc<Capabilities>,
2182 target_caps: &Arc<Capabilities>,
2183 use_atomic: bool,
2184 _vdo_stall_threshold: u32,
2185 source_uncached: bool,
2186 target_uncached: bool,
2187 governor: Option<Arc<Governor>>,
2188 _target_label: String,
2189 fsync_tracker: &mut FsyncLatencyTracker,
2190 buffer_limit: Option<usize>,
2191 skip_fsync: bool,
2192 segment_stall_timeout_secs: u64,
2193 segment_overall_timeout_secs: u64,
2194 use_fixed: bool,
2195 cached_segments: Option<Vec<FileSegment>>,
2196 ) -> Result<CopyStats> {
2197 debug!("perform_delta_uring_pipelined: Start offset={} len={}", offset, length);
2198 let mut stats = CopyStats::default();
2199 let start_time = Instant::now();
2200
2201 let use_seek_hole = source_caps.seek_hole.load(Ordering::Relaxed);
2202
2203 if use_seek_hole {
2204 let segments = if let Some(segs) = cached_segments {
2205 debug!("perform_delta_uring_pipelined: Using pre-computed {} segments from probe", segs.len());
2206 segs
2207 } else {
2208 debug!("perform_delta_uring_pipelined: Mapping sparse segments...");
2209 let mapped = spawn_blocking(move || {
2210 Self::map_sparse_segments(sfd, offset, length)
2211 }).await.map_err(FxcpError::Join)??;
2212 debug!("perform_delta_uring_pipelined: Mapped {} segments", mapped.len());
2213 mapped
2214 };
2215
2216 for segment in segments {
2217 match segment {
2218 FileSegment::Hole { offset, len } => {
2219 stats.bytes_zeros += len;
2220 if len >= crate::constants::MIN_HOLE_PUNCH_SIZE {
2221 let falloc_op = if use_fixed {
2222 opcode::Fallocate::new(types::Fixed(DST_FIXED_SLOT), len)
2223 .offset(offset)
2224 .mode(libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE)
2225 .build().user_data(HOLE_OP)
2226 } else {
2227 opcode::Fallocate::new(types::Fd(dfd), len)
2228 .offset(offset)
2229 .mode(libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE)
2230 .build().user_data(HOLE_OP)
2231 };
2232
2233 let mut pushed = false;
2234 for _ in 0..10 {
2235 if unsafe { ring.submission().push(&falloc_op) }.is_ok() {
2237 pushed = true;
2238 break;
2239 }
2240 let _ = ring.submit();
2241 if ring.submission().is_full() {
2242 match tokio::time::timeout(Duration::from_millis(crate::constants::IO_URING_READABLE_TIMEOUT_MS), async_fd.readable()).await {
2243 Ok(Ok(mut guard)) => {
2244 let mut buf = [0u8; 8];
2245 let _ = unsafe { libc::read(async_fd.get_ref().as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, 8) };
2247 guard.clear_ready();
2248 },
2249 Ok(Err(e)) => return Err(FxcpError::Io(e)),
2250 Err(_) => {
2251 if let Some(_) = ring.completion().next() {
2252 }
2254 }
2255 }
2256 }
2257 }
2258 if !pushed {
2259 return Err(FxcpError::Io(io::Error::other("Submission queue full (fallocate)")));
2260 }
2261 }
2262 },
2263 FileSegment::Data { offset, len } => {
2264 let atomic_pool_ref = atomic_pool.as_deref_mut();
2265 let seg_stats = Self::process_data_segment(
2266 ring, sfd, dfd, offset, len, vdo_opt, buffer_pool, atomic_pool_ref, async_fd.clone(),
2267 source_caps, target_caps, use_atomic,
2268 source_uncached, target_uncached, &governor,
2269 &_target_label, buffer_limit,
2270 segment_stall_timeout_secs, segment_overall_timeout_secs,
2271 use_fixed,
2272 ).await?;
2273 stats.bytes_processed += seg_stats.bytes_processed;
2274 stats.bytes_zeros += seg_stats.bytes_zeros;
2275 stats.ops_count += seg_stats.ops_count;
2276 }
2277 }
2278 }
2279 } else {
2280 let segment_stats = Self::process_data_segment(
2281 ring, sfd, dfd, offset, length, vdo_opt, buffer_pool, atomic_pool, async_fd.clone(),
2282 source_caps, target_caps, use_atomic,
2283 source_uncached, target_uncached, &governor,
2284 &_target_label, buffer_limit,
2285 segment_stall_timeout_secs, segment_overall_timeout_secs,
2286 use_fixed,
2287 ).await?;
2288 stats.bytes_processed += segment_stats.bytes_processed;
2289 stats.bytes_zeros += segment_stats.bytes_zeros;
2290 stats.ops_count += segment_stats.ops_count;
2291 }
2292
2293 uring_fsync_with_timeout(ring, dfd, use_fixed, &async_fd, fsync_tracker, &_debug_path, skip_fsync).await?;
2294
2295 metrics::COPY_METHOD_STANDARD.with_label_values(&[&_target_label]).inc();
2296 stats.io_duration = start_time.elapsed();
2297 debug!("perform_delta_uring_pipelined: Finished");
2298 Ok(stats)
2299 }
2300
2301 async fn process_data_segment(
2302 ring: &mut IoUring,
2303 sfd: i32,
2304 dfd: i32,
2305 start_offset: u64,
2306 total_len: u64,
2307 vdo_opt: bool,
2308 buffer_pool: &mut BufferPool,
2309 atomic_pool: Option<&mut BufferPool>,
2310 async_fd: Arc<AsyncFd<RawFd>>,
2311 source_caps: &Arc<Capabilities>,
2312 target_caps: &Arc<Capabilities>,
2313 use_atomic: bool,
2314 source_uncached: bool,
2315 target_uncached: bool,
2316 governor: &Option<Arc<Governor>>,
2317 _target_label: &String,
2318 buffer_limit: Option<usize>,
2319 segment_stall_timeout_secs: u64,
2320 segment_overall_timeout_secs: u64,
2321 use_fixed: bool,
2322 ) -> Result<CopyStats> {
2323 debug!("process_data_segment ENTRY: offset={}, len={}", start_offset, total_len);
2324
2325 let mut bytes_processed = 0u64;
2326 let mut bytes_zeros = 0u64;
2327 let mut ops_count = 0u64;
2328
2329 let (active_pool, is_atomic_buffer) = if use_atomic {
2330 if let Some(pool) = atomic_pool {
2331 (pool, true)
2332 } else {
2333 (buffer_pool, true)
2334 }
2335 } else {
2336 (buffer_pool, false)
2337 };
2338
2339 let chunk_size = active_pool.chunk_size() as u64;
2340 let capacity = active_pool.capacity();
2341 let buffers_count_u64 = buffer_limit.unwrap_or(capacity).min(capacity) as u64;
2342
2343 let mut inflight_ops = 0;
2344 let mut offset = start_offset;
2345 let mut length = total_len;
2346 let mut atomic_intent = vec![false; capacity];
2347 let mut context_map = vec![0u64; capacity];
2348 let mut encountered_error: Option<FxcpError> = None;
2349 let atomic_max = target_caps.atomic_max_bytes.load(Ordering::Relaxed) as u64;
2350 let mut pending_submissions = 0;
2351
2352 let mut batched_fallback_counter = metrics::BatchedCounter::new(
2353 metrics::ATOMIC_WRITE_FALLBACKS.clone(),
2354 64
2355 );
2356
2357 let loop_start = std::time::Instant::now();
2358 let mut last_progress = std::time::Instant::now();
2359 let mut last_bytes_processed = 0u64;
2360
2361 while length > 0 || inflight_ops > 0 {
2362 if encountered_error.is_some() && inflight_ops == 0 {
2366 break;
2367 }
2368
2369 if loop_start.elapsed() > std::time::Duration::from_secs(segment_overall_timeout_secs) {
2370 error!("process_data_segment: Segment timeout after {}s (offset={}, len={}, inflight={})",
2371 segment_overall_timeout_secs, offset, length, inflight_ops);
2372 return Err(FxcpError::Io(io::Error::new(
2373 io::ErrorKind::TimedOut,
2374 format!("Segment timeout: {}s with {} ops in flight", segment_overall_timeout_secs, inflight_ops)
2375 )));
2376 }
2377
2378 if bytes_processed > last_bytes_processed {
2379 last_progress = std::time::Instant::now();
2380 last_bytes_processed = bytes_processed;
2381 } else if inflight_ops > 0 && last_progress.elapsed() > std::time::Duration::from_secs(segment_stall_timeout_secs) {
2382 error!("process_data_segment: STALL detected - no progress for {}s (inflight={}, free_buffers={}, sq_capacity={})",
2383 segment_stall_timeout_secs, inflight_ops, active_pool.free_count(), ring.submission().capacity());
2384 return Err(FxcpError::Io(io::Error::other(
2385 format!("Stall: no progress for {}s (inflight={}, free={})",
2386 segment_stall_timeout_secs, inflight_ops, active_pool.free_count())
2387 )));
2388 }
2389
2390 if let Some(gov) = &governor
2391 && gov.current_memory_usage_pct() > 0.90 { tokio::task::yield_now().await; }
2392
2393 while inflight_ops < buffers_count_u64 && length > 0 && encountered_error.is_none() {
2394 if ring.submission().is_full() {
2395 if pending_submissions > 0 {
2396 if let Err(e) = ring.submit() {
2397 encountered_error = Some(FxcpError::Io(io::Error::other(e.to_string())));
2398 break;
2399 }
2400 pending_submissions = 0;
2401 }
2402 if ring.submission().is_full() {
2403 break;
2404 }
2405 }
2406
2407 let index = if let Some(idx) = active_pool.acquire() { idx } else {
2408 break;
2409 };
2410
2411 if index as usize >= active_pool.capacity() {
2412 error!("BufferPool Index Out of Bounds: {} (Capacity: {})", index, active_pool.capacity());
2413 return Err(FxcpError::MemoryExhausted("BufferPool index corrupted".into()));
2414 }
2415
2416 let mut current_len = u32::try_from((length as usize).min(chunk_size as usize))
2417 .map_err(|_| FxcpError::Config(format!(
2418 "io_uring chunk length {} exceeds u32::MAX", (length as usize).min(chunk_size as usize)
2419 )))?;
2420
2421 if use_atomic && is_atomic_buffer && atomic_max > 0 {
2422 current_len = current_len.min(u32::try_from(atomic_max)
2423 .map_err(|_| FxcpError::Config(format!(
2424 "atomic_max {} exceeds u32::MAX", atomic_max
2425 )))?);
2426 }
2427 let len = current_len;
2428
2429 if use_atomic && is_atomic_buffer {
2430 let ptr_opt = active_pool.get_ptr(index);
2431 if let Some(ptr) = ptr_opt {
2432 let ptr_addr = ptr as usize;
2433 if let Err(e) = Self::check_atomic_invariants(target_caps, offset, len, ptr_addr) {
2434 active_pool.release(index);
2435 return Err(e);
2436 }
2437 } else {
2438 active_pool.release(index);
2439 error!("BufferPool returned null pointer for index {}", index);
2440 return Err(FxcpError::MemoryExhausted("BufferPool null pointer".into()));
2441 }
2442 atomic_intent[index as usize] = true;
2443 } else {
2444 atomic_intent[index as usize] = false;
2445 }
2446
2447 let mut read_flags: i32 = 0;
2448 if source_uncached && source_caps.uncached_io.load(Ordering::Relaxed) { read_flags |= RWF_UNCACHED; }
2449
2450 context_map[index as usize] = offset;
2451 let buf_ptr = match active_pool.get_ptr(index) {
2452 Some(p) => p,
2453 None => {
2454 active_pool.release(index);
2455 error!("BufferPool returned null pointer for index {}", index);
2456 return Err(FxcpError::MemoryExhausted("BufferPool null pointer".into()));
2457 }
2458 };
2459
2460 let user_data = READ_OP | (index as u64);
2461 let read_op = if use_fixed {
2462 opcode::ReadFixed::new(types::Fixed(SRC_FIXED_SLOT), buf_ptr, len, index)
2463 .offset(offset).rw_flags(read_flags).build().user_data(user_data)
2464 } else {
2465 opcode::Read::new(types::Fd(sfd), buf_ptr, len)
2466 .offset(offset).rw_flags(read_flags).build().user_data(user_data)
2467 };
2468
2469 if unsafe { ring.submission().push(&read_op) }.is_err() {
2471 active_pool.release(index);
2472 if pending_submissions > 0 {
2473 let _ = ring.submit();
2474 pending_submissions = 0;
2475 }
2476 break;
2477 }
2478 inflight_ops += 1;
2479 pending_submissions += 1;
2480
2481 offset += len as u64;
2482 length -= len as u64;
2483 ops_count += 1;
2484 }
2485
2486 if pending_submissions > 0 {
2487 if let Err(e) = ring.submit()
2488 && encountered_error.is_none() {
2489 encountered_error = Some(FxcpError::Io(io::Error::other(e.to_string())));
2490 }
2491 pending_submissions = 0;
2492 }
2493
2494 loop {
2495 let peek_cqe = ring.completion().next();
2496
2497 let cqe = if let Some(c) = peek_cqe {
2498 c
2499 } else {
2500 if length > 0 && active_pool.free_count() > 0 && !ring.submission().is_full() {
2501 break;
2502 }
2503
2504 if inflight_ops > 0 {
2505 static TIMEOUT_STREAK: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2506
2507 match tokio::time::timeout(Duration::from_millis(crate::constants::IO_URING_READABLE_TIMEOUT_MS), async_fd.readable()).await {
2508 Ok(Ok(mut guard)) => {
2509 TIMEOUT_STREAK.store(0, std::sync::atomic::Ordering::Relaxed);
2510 let mut buf = [0u8; 8];
2511 let _ = unsafe { libc::read(async_fd.get_ref().as_raw_fd(), buf.as_mut_ptr() as *mut libc::c_void, 8) };
2513 guard.clear_ready();
2514 if let Some(c) = ring.completion().next() { c } else { continue; }
2515 },
2516 Ok(Err(e)) => return Err(FxcpError::Io(e)),
2517 Err(_timeout) => {
2518 let streak = TIMEOUT_STREAK.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
2519 if streak >= crate::constants::IOURING_COMPLETION_TIMEOUT_STREAK {
2520 let sq_len = ring.submission().len();
2521 let sq_cap = ring.submission().capacity();
2522 let cq_len = ring.completion().len();
2523 error!("process_data_segment: io_uring hung - no completions for 60s (inflight={}, sq_len={}, sq_cap={}, cq_len={})",
2524 inflight_ops, sq_len, sq_cap, cq_len);
2525 return Err(FxcpError::Io(io::Error::new(
2526 io::ErrorKind::TimedOut,
2527 "io_uring hung - no completions for 60s"
2528 )));
2529 }
2530 ring.submit()?;
2531 if let Some(c) = ring.completion().next() { c } else { continue; }
2532 }
2533 }
2534 } else {
2535 break;
2536 }
2537 };
2538
2539 let user_data = cqe.user_data();
2540 if user_data == 0 {
2541 continue;
2542 }
2543
2544 let op_type = user_data & OP_TYPE_MASK;
2545 if op_type == HOLE_OP {
2546 continue;
2547 }
2548
2549 let index = (user_data & INDEX_MASK) as u16;
2550 let original_offset = if (index as usize) < context_map.len() {
2551 context_map[index as usize]
2552 } else {
2553 0
2554 };
2555
2556 if cqe.result() < 0 {
2557 let raw_err = -cqe.result();
2558
2559 if op_type == WRITE_OP && atomic_intent[index as usize]
2560 && (raw_err == libc::EOPNOTSUPP || raw_err == libc::EINVAL) {
2561 if raw_err == libc::EOPNOTSUPP {
2562 warn!("Atomic Write Failed (EOPNOTSUPP). Disabling Atomic Writes for this session.");
2563 target_caps.atomic_writes.store(false, Ordering::Relaxed);
2564 } else {
2565 warn!("Atomic Write Failed for offset {}: {} (Errno {}). Downgrading to Buffered I/O.", context_map[index as usize], io::Error::from_raw_os_error(raw_err), raw_err);
2566 }
2567
2568 batched_fallback_counter.inc();
2569 atomic_intent[index as usize] = false;
2570
2571 let retry_len = active_pool.get_len(index) as u32;
2572 let retry_offset = context_map[index as usize];
2573
2574 let mut write_flags: i32 = 0;
2575 if target_uncached && target_caps.uncached_io.load(Ordering::Relaxed) { write_flags |= RWF_UNCACHED; }
2576
2577 let user_data_write = WRITE_OP | (index as u64);
2578 if let Some(buf_ptr) = active_pool.get_ptr(index) {
2579 let write_op = unsafe { build_write_sqe(use_fixed, dfd, buf_ptr, retry_len, index as u16, retry_offset, write_flags, user_data_write) };
2581
2582 if unsafe { ring.submission().push(&write_op) }.is_ok() {
2584 continue;
2585 } else {
2586 encountered_error = Some(FxcpError::Io(io::Error::other("SQ full during atomic retry")));
2587 }
2588 } else {
2589 error!("io_uring buffer pool index out of range (atomic retry): index={}", index);
2590 encountered_error = Some(FxcpError::Io(io::Error::other("io_uring buffer pool index out of range (atomic retry)")));
2591 }
2592 }
2593
2594 if op_type != FALLOC_OP {
2595 active_pool.release(index);
2596 }
2597
2598 if encountered_error.is_none() {
2599 let err = io::Error::from_raw_os_error(-cqe.result());
2600 if err.kind() != io::ErrorKind::Interrupted {
2601 encountered_error = Some(FxcpError::Io(err));
2602 }
2603 }
2604 if op_type != FADVISE_OP {
2607 inflight_ops -= 1;
2608 }
2609 continue;
2610 }
2611
2612 if encountered_error.is_some() {
2613 if op_type != FALLOC_OP {
2614 active_pool.release(index);
2615 }
2616 if op_type != FADVISE_OP {
2618 inflight_ops -= 1;
2619 }
2620 continue;
2621 }
2622
2623 let bytes_transferred = cqe.result() as u64;
2624
2625 match op_type {
2626 READ_OP => {
2627 inflight_ops -= 1;
2628 let mut wf: i32 = 0;
2629 if target_uncached && target_caps.uncached_io.load(Ordering::Relaxed) { wf |= RWF_UNCACHED; }
2630 if target_caps.atomic_writes.load(Ordering::Relaxed) && atomic_intent[index as usize] { wf |= RWF_ATOMIC; }
2631
2632 match handle_read_completion(ring, active_pool, index, bytes_transferred, original_offset, dfd, use_fixed, vdo_opt, wf) {
2633 Ok((zeros_delta, processed_delta)) => {
2634 pending_submissions += 1;
2635 inflight_ops += 1;
2636 bytes_zeros += zeros_delta;
2637 bytes_processed += processed_delta;
2638 }
2639 Err(e) => {
2640 encountered_error = Some(e);
2641 continue;
2642 }
2643 }
2644 },
2645 WRITE_OP => {
2646 inflight_ops -= 1;
2647 bytes_processed += bytes_transferred;
2648 active_pool.release(index);
2649 },
2650 FALLOC_OP => {
2651 inflight_ops -= 1;
2652 active_pool.release(index);
2653 }
2654 _ => {
2655 warn!("process_data_segment: Unknown CQE op type {:x} (user_data={:x}), not tracked in inflight_ops — ignoring", op_type, user_data);
2656 }
2660 }
2661 }
2662 }
2663
2664 if let Some(err) = encountered_error {
2665 debug!("process_data_segment EXIT with ERROR: {:?}", err);
2666 return Err(err);
2667 }
2668
2669 debug!("process_data_segment EXIT SUCCESS");
2670 Ok(CopyStats { bytes_processed, bytes_zeros, io_duration: Duration::ZERO, ops_count })
2671 }
2672
2673 #[inline(always)]
2674 fn _is_block_zero(buf: &[u8]) -> bool {
2675 is_zero_block(buf)
2676 }
2677}
2678
2679pub fn apply_lock(path: &Path, lock_type: u32, lock_map: &DashMap<u64, std::fs::File>) -> Result<()> {
2681 if (lock_type & libc::LOCK_UN as u32) != 0 {
2682 if let Ok(meta) = std::fs::metadata(path) { lock_map.remove(&meta.ino()); }
2683 return Ok(());
2684 }
2685
2686 let file = std::fs::File::open(path).map_err(FxcpError::Io)?;
2687 let fd = file.as_raw_fd();
2688
2689 let ret = unsafe { libc::flock(fd, (lock_type as i32) | libc::LOCK_NB) };
2692
2693 if ret == 0 { if let Ok(meta) = file.metadata() { lock_map.insert(meta.ino(), file); } }
2694 else { debug!("Failed to apply lock on {:?}: {}", path, std::io::Error::last_os_error()); }
2695
2696 Ok(())
2697}
2698
2699impl SmartCopier {
2703 pub async fn copy_delta(
2706 &mut self,
2707 src: &std::path::Path,
2708 dst: &std::path::Path,
2709 dirty_ranges: &[crate::hashing::DirtyRange],
2710 file_size: u64,
2711 label: &str,
2712 ) -> Result<CopyStats> {
2713 let mut total_stats = CopyStats::default();
2714
2715 for range in dirty_ranges {
2716 if range.offset >= file_size {
2717 continue;
2718 }
2719 let stats = self.optimized_copy_range(
2720 src.to_path_buf(),
2721 dst.to_path_buf(),
2722 range.offset,
2723 range.length,
2724 file_size,
2725 label.to_string(),
2726 None,
2727 self.skip_fsync,
2728 ).await?;
2729 total_stats.bytes_processed += stats.bytes_processed;
2730 total_stats.ops_count += stats.ops_count;
2731 }
2732
2733 if let Ok(dst_meta) = std::fs::metadata(dst)
2734 && dst_meta.len() > file_size {
2735 let stats = self.optimized_truncate(dst.to_path_buf(), file_size).await?;
2736 total_stats.bytes_processed += stats.bytes_processed;
2737 total_stats.ops_count += stats.ops_count;
2738 }
2739
2740 Ok(total_stats)
2741 }
2742}
2743
2744#[cfg(test)]
2745mod tests {
2746 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2747 use super::*;
2748
2749 #[tokio::test]
2750 #[ignore = "flaky under parallel NFS load; run isolated with RUST_TEST_THREADS=1"]
2751 async fn test_iouring_short_read_no_corruption() {
2752 crate::metrics::initialize_metrics(512);
2753
2754 let dir = tempfile::Builder::new()
2757 .prefix("fxcp_test_")
2758 .tempdir_in("/var/tmp")
2759 .unwrap();
2760 let src_path = dir.path().join("source.bin");
2761 let dst_path = dir.path().join("dest.bin");
2762
2763 let data: Vec<u8> = (0..100_001u32).map(|i| (i % 251) as u8).collect();
2764 std::fs::write(&src_path, &data).unwrap();
2765
2766 let mut copier = crate::sync::create_copier(dir.path(), dir.path())
2767 .await
2768 .unwrap();
2769 copier
2770 .optimized_copy(
2771 src_path.clone(),
2772 dst_path.clone(),
2773 100_001,
2774 "test".to_string(),
2775 None,
2776 true,
2777 false,
2778 )
2779 .await
2780 .unwrap();
2781
2782 let src_hash = blake3::hash(&std::fs::read(&src_path).unwrap());
2783 let dst_hash = blake3::hash(&std::fs::read(&dst_path).unwrap());
2784 assert_eq!(
2785 src_hash, dst_hash,
2786 "Copy must be byte-identical (short read corruption check)"
2787 );
2788
2789 let src_len = std::fs::metadata(&src_path).unwrap().len();
2790 let dst_len = std::fs::metadata(&dst_path).unwrap().len();
2791 assert_eq!(src_len, dst_len, "File sizes must match");
2792 }
2793
2794 #[tokio::test]
2795 async fn test_copy_delta_truncates_on_shrink() {
2796 crate::metrics::initialize_metrics(512);
2797 let dir = tempfile::tempdir().unwrap();
2798 let src = dir.path().join("src.bin");
2799 let dst = dir.path().join("dst.bin");
2800 std::fs::write(&src, vec![0xAAu8; 100_000]).unwrap();
2801 std::fs::write(&dst, vec![0xAAu8; 200_000]).unwrap();
2802
2803 let mut copier = crate::sync::create_copier(dir.path(), dir.path())
2804 .await
2805 .unwrap();
2806 copier.copy_delta(&src, &dst, &[], 100_000, "test").await.unwrap();
2807
2808 let dst_len = std::fs::metadata(&dst).unwrap().len();
2809 assert_eq!(dst_len, 100_000, "dst should be truncated to source size");
2810 }
2811
2812 #[tokio::test]
2813 async fn test_copy_delta_shrink_to_zero() {
2814 crate::metrics::initialize_metrics(512);
2815 let dir = tempfile::tempdir().unwrap();
2816 let src = dir.path().join("src.bin");
2817 let dst = dir.path().join("dst.bin");
2818 std::fs::write(&src, b"").unwrap();
2819 std::fs::write(&dst, vec![0xBBu8; 100_000]).unwrap();
2820
2821 let mut copier = crate::sync::create_copier(dir.path(), dir.path())
2822 .await
2823 .unwrap();
2824 copier.copy_delta(&src, &dst, &[], 0, "test").await.unwrap();
2825
2826 let dst_len = std::fs::metadata(&dst).unwrap().len();
2827 assert_eq!(dst_len, 0, "dst should be truncated to 0");
2828 }
2829
2830 #[tokio::test]
2831 async fn test_copy_delta_no_truncate_on_grow() {
2832 crate::metrics::initialize_metrics(512);
2833 let dir = tempfile::tempdir().unwrap();
2834 let src = dir.path().join("src.bin");
2835 let dst = dir.path().join("dst.bin");
2836 let src_data = vec![0xCCu8; 200_000];
2837 std::fs::write(&src, &src_data).unwrap();
2838 std::fs::write(&dst, vec![0xCCu8; 100_000]).unwrap();
2839
2840 let dirty = vec![crate::hashing::DirtyRange { offset: 100_000, length: 100_000 }];
2841 let mut copier = crate::sync::create_copier(dir.path(), dir.path())
2842 .await
2843 .unwrap();
2844 copier.copy_delta(&src, &dst, &dirty, 200_000, "test").await.unwrap();
2845
2846 let dst_len = std::fs::metadata(&dst).unwrap().len();
2847 assert_eq!(dst_len, 200_000, "dst should grow to 200KB, not be truncated");
2848 }
2849
2850 #[test]
2851 fn test_prepare_destination_file_sparse_skip() {
2852 crate::metrics::initialize_metrics(512);
2853 let dir = tempfile::tempdir().unwrap();
2854 let path = dir.path().join("sparse_target.dat");
2855
2856 let (file, _fd, _) = SmartCopier::prepare_destination_file(
2858 path.clone(), false, false, 1_048_576, true, None, false
2859 ).unwrap();
2860 drop(file);
2861
2862 use std::os::unix::fs::MetadataExt;
2863 let meta = std::fs::metadata(&path).unwrap();
2864 assert_eq!(meta.len(), 0, "truncate(true) sets size to 0, no fallocate for sparse");
2866 assert!(meta.blocks() < 16, "sparse file should have minimal block allocation");
2867 }
2868
2869 #[test]
2870 fn test_prepare_destination_file_non_sparse_preallocates() {
2871 crate::metrics::initialize_metrics(512);
2872 let dir = tempfile::tempdir().unwrap();
2873 let path = dir.path().join("dense_target.dat");
2874
2875 let (file, _fd, _) = SmartCopier::prepare_destination_file(
2877 path.clone(), false, false, 65536, false, None, false
2878 ).unwrap();
2879 drop(file);
2880
2881 use std::os::unix::fs::MetadataExt;
2882 let meta = std::fs::metadata(&path).unwrap();
2883 assert!(meta.len() == 0 || meta.blocks() > 0,
2888 "non-sparse destination should have blocks after fallocate (or be on tmpfs)");
2889 }
2890
2891 #[tokio::test]
2892 async fn test_inflight_counter_balanced_after_copy() {
2893 crate::metrics::initialize_metrics(512);
2896 let dir = tempfile::tempdir().unwrap();
2897 let src_path = dir.path().join("source_balanced.bin");
2898 let dst_path = dir.path().join("dest_balanced.bin");
2899
2900 let data: Vec<u8> = (0..200_003u32).map(|i| (i % 251) as u8).collect();
2902 std::fs::write(&src_path, &data).unwrap();
2903
2904 let mut copier = crate::sync::create_copier(dir.path(), dir.path())
2905 .await
2906 .unwrap();
2907 copier
2909 .optimized_copy(
2910 src_path.clone(),
2911 dst_path.clone(),
2912 200_003,
2913 "test_balanced".to_string(),
2914 None,
2915 true,
2916 false,
2917 )
2918 .await
2919 .unwrap();
2920
2921 let src_hash = blake3::hash(&std::fs::read(&src_path).unwrap());
2923 let dst_hash = blake3::hash(&std::fs::read(&dst_path).unwrap());
2924 assert_eq!(src_hash, dst_hash, "Copy must be byte-identical (inflight balance check)");
2925 }
2926
2927 #[tokio::test]
2928 async fn test_inflight_unknown_op_type_no_hang() {
2929 crate::metrics::initialize_metrics(512);
2937 let dir = tempfile::tempdir().unwrap();
2938 let src_path = dir.path().join("source_nohang.bin");
2939 let dst_path = dir.path().join("dest_nohang.bin");
2940
2941 let data: Vec<u8> = (0..153_600u32).map(|i| (i % 127) as u8).collect();
2943 std::fs::write(&src_path, &data).unwrap();
2944
2945 let mut copier = crate::sync::create_copier(dir.path(), dir.path())
2946 .await
2947 .unwrap();
2948
2949 let result = tokio::time::timeout(
2952 std::time::Duration::from_secs(10),
2953 copier.optimized_copy(
2954 src_path.clone(),
2955 dst_path.clone(),
2956 153_600,
2957 "test_nohang".to_string(),
2958 None,
2959 true,
2960 false,
2961 ),
2962 )
2963 .await;
2964
2965 assert!(result.is_ok(), "Copy timed out — possible inflight_ops hang (unknown op type bug)");
2966 assert!(result.unwrap().is_ok(), "Copy failed with error");
2967
2968 let src_hash = blake3::hash(&std::fs::read(&src_path).unwrap());
2969 let dst_hash = blake3::hash(&std::fs::read(&dst_path).unwrap());
2970 assert_eq!(src_hash, dst_hash, "Copy must be byte-identical");
2971 }
2972
2973 #[tokio::test]
2974 async fn test_separate_postcopy_ring_no_cqe_leak() {
2975 use std::os::unix::io::AsRawFd;
2976 let ring = io_uring::IoUring::new(256).unwrap();
2977 let ring_fd = ring.as_raw_fd();
2978 let postcopy_ring: io_uring::IoUring = io_uring::IoUring::builder()
2979 .setup_attach_wq(ring_fd)
2980 .build(8)
2981 .unwrap();
2982 assert_ne!(ring.as_raw_fd(), postcopy_ring.as_raw_fd(),
2983 "postcopy ring must have a different fd from data ring");
2984
2985 crate::metrics::initialize_metrics(512);
2986 let dir = tempfile::tempdir().unwrap();
2987 let src_path = dir.path().join("src_postcopy.bin");
2988 let dst_path = dir.path().join("dst_postcopy.bin");
2989 let data: Vec<u8> = (0..128 * 1024u32).map(|i| (i % 251) as u8).collect();
2990 std::fs::write(&src_path, &data).unwrap();
2991
2992 let mut copier = crate::sync::create_copier(dir.path(), dir.path())
2993 .await
2994 .unwrap();
2995
2996 let result = tokio::time::timeout(
2997 std::time::Duration::from_secs(10),
2998 copier.optimized_copy(
2999 src_path.clone(), dst_path.clone(),
3000 128 * 1024, "test_postcopy".to_string(),
3001 None, false, false,
3002 ),
3003 ).await;
3004
3005 assert!(result.is_ok(), "Copy timed out — postcopy ring may be broken");
3006 assert!(result.unwrap().is_ok(), "Copy failed");
3007 let src_hash = blake3::hash(&std::fs::read(&src_path).unwrap());
3008 let dst_hash = blake3::hash(&std::fs::read(&dst_path).unwrap());
3009 assert_eq!(src_hash, dst_hash, "Copy must be byte-identical");
3010 }
3011
3012 #[test]
3013 fn test_iouring_read_write_fallback_no_ebadf() {
3014 use std::os::unix::io::AsRawFd;
3016 let dir = tempfile::Builder::new()
3017 .prefix("fxcp_iouring_fallback_")
3018 .tempdir_in("/var/tmp")
3019 .unwrap();
3020 let src_path = dir.path().join("read_test.bin");
3021 let dst_path = dir.path().join("write_test.bin");
3022 let payload = b"EBADF fallback verification payload";
3023 std::fs::write(&src_path, payload).unwrap();
3024 std::fs::write(&dst_path, vec![0u8; payload.len()]).unwrap();
3025
3026 let src_file = std::fs::File::open(&src_path).unwrap();
3027 let dst_file = std::fs::OpenOptions::new()
3028 .write(true)
3029 .open(&dst_path)
3030 .unwrap();
3031 let sfd = src_file.as_raw_fd();
3032 let dfd = dst_file.as_raw_fd();
3033
3034 let mut ring = io_uring::IoUring::new(8).unwrap();
3035 let mut read_buf = vec![0u8; payload.len()];
3039 let read_sqe = opcode::Read::new(types::Fd(sfd), read_buf.as_mut_ptr(), read_buf.len() as u32)
3040 .offset(0)
3041 .build()
3042 .user_data(1);
3043
3044 unsafe { ring.submission().push(&read_sqe).unwrap(); }
3045 ring.submit_and_wait(1).unwrap();
3046
3047 let read_cqe = ring.completion().next().unwrap();
3048 assert!(
3050 read_cqe.result() >= 0,
3051 "Read(Fd) without register_files returned error {}: expected success (EBADF root cause would give -9)",
3052 read_cqe.result()
3053 );
3054 assert_eq!(read_cqe.result() as usize, payload.len());
3055
3056 let write_sqe = opcode::Write::new(types::Fd(dfd), read_buf.as_ptr(), read_buf.len() as u32)
3057 .offset(0)
3058 .build()
3059 .user_data(2);
3060
3061 unsafe { ring.submission().push(&write_sqe).unwrap(); }
3062 ring.submit_and_wait(1).unwrap();
3063
3064 let write_cqe = ring.completion().next().unwrap();
3065 assert!(
3067 write_cqe.result() >= 0,
3068 "Write(Fd) without register_files returned error {}: expected success (EBADF root cause would give -9)",
3069 write_cqe.result()
3070 );
3071 assert_eq!(write_cqe.result() as usize, payload.len());
3072
3073 let written = std::fs::read(&dst_path).unwrap();
3075 assert_eq!(&written, payload, "Written content must match source payload");
3076 }
3077
3078 #[test]
3079 fn test_reflink_or_copy_preserves_content() {
3080 let dir = tempfile::tempdir().unwrap();
3082 let src = dir.path().join("src.bin");
3083 let dst = dir.path().join("dst.bin");
3084 let data: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
3085 std::fs::write(&src, &data).unwrap();
3086 let bytes = reflink_or_copy(&src, &dst).unwrap();
3087 assert_eq!(bytes, data.len() as u64);
3088 let result = std::fs::read(&dst).unwrap();
3089 assert_eq!(result, data, "reflink_or_copy must preserve content exactly");
3090 }
3091
3092 #[test]
3093 fn test_reflink_or_copy_zero_byte_file() {
3094 let dir = tempfile::tempdir().unwrap();
3096 let src = dir.path().join("empty.bin");
3097 let dst = dir.path().join("empty_dst.bin");
3098 std::fs::write(&src, b"").unwrap();
3099 let bytes = reflink_or_copy(&src, &dst).unwrap();
3100 assert_eq!(bytes, 0);
3101 assert_eq!(std::fs::metadata(&dst).unwrap().len(), 0);
3102 }
3103
3104 #[test]
3105 fn test_reflink_or_copy_missing_src_returns_error() {
3106 let dir = tempfile::tempdir().unwrap();
3108 let src = dir.path().join("nonexistent.bin");
3109 let dst = dir.path().join("dst.bin");
3110 let result = reflink_or_copy(&src, &dst);
3111 assert!(result.is_err(), "missing src must return Err");
3112 }
3113
3114 #[test]
3115 fn test_reflink_or_copy_same_device_xfs_shared_extents() {
3116 use std::process::Command;
3119 let dir = tempfile::Builder::new()
3120 .prefix("fxcp_reflink_test_")
3121 .tempdir_in("/var/tmp")
3122 .unwrap();
3123 let src = dir.path().join("src.bin");
3124 let dst = dir.path().join("dst.bin");
3125 let data = vec![0xABu8; 65536];
3127 std::fs::write(&src, &data).unwrap();
3128 let bytes = reflink_or_copy(&src, &dst).unwrap();
3129 assert_eq!(bytes, 65536);
3130 assert_eq!(std::fs::read(&dst).unwrap(), data);
3132 let out = Command::new("filefrag").arg("-v").arg(&dst).output();
3134 if let Ok(out) = out {
3135 let s = String::from_utf8_lossy(&out.stdout);
3136 if s.contains("shared") {
3137 println!("FICLONE confirmed: shared extents detected");
3139 }
3140 }
3143 }
3144}