Skip to main content

fxcp_core/operations/
mod.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2// Copyright (C) 2025 Joel Wirāmu Pauling <aenertia@aenertia.net>
3//
4//! SmartCopier, io_uring pipeline, reflink/CoW.
5
6/// SIMD zero-block detection (AVX-512, AVX2, NEON, scalar fallback).
7pub mod simd;
8/// Filesystem capability probing and copy tier selection.
9pub mod capabilities;
10/// Container and device-mapper storage stack detection.
11pub mod container;
12/// io_uring NAPI busy-poll registration for NFS-target rings.
13pub 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// Planned btrfs snapshot integration
69#[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/// Tracks fsync latency with exponential moving average for adaptive timeout calculation
88#[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    /// Record a successful fsync and update the moving average
105    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    /// Record a timeout and double the average estimate
113    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    /// Calculate an adaptive timeout based on observed latency (avg + 4*deviation, clamped 5s..60s)
119    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// Planned btrfs snapshot integration
126#[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/// Statistics collected during a copy operation
173#[derive(Debug, Default, Clone, Copy)]
174pub struct CopyStats {
175    /// Total bytes read and written
176    pub bytes_processed: u64,
177    /// Bytes detected as all-zero (hole-punched instead of written)
178    pub bytes_zeros: u64,
179    /// Wall-clock time spent on I/O
180    pub io_duration: Duration,
181    /// Number of io_uring operations submitted
182    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
202/// Copy src to dst using reflink (FICLONE) if supported, falling back to
203/// sendfile and finally a read+write loop. Returns bytes copied.
204///
205/// This is the synchronous small-file helper for code paths that cannot use
206/// the full async SmartCopier (e.g. snapshot creation, tree metadata copies).
207/// Never use `std::fs::copy()`  --  it bypasses CoW and reflink entirely.
208pub 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    // Tier 1: Try FICLONE (instant CoW reflink)
224    // SAFETY: sfd and dfd are valid open file descriptors from File::open above.
225    let ret = unsafe { libc::ioctl(dfd, FICLONE as _, sfd) };
226    if ret == 0 {
227        return Ok(size);
228    }
229
230    // Tier 2: sendfile (kernel-space copy, no userspace buffer)
231    let mut total: u64 = 0;
232    while total < size {
233        let remaining = size - total;
234        // SAFETY: sfd and dfd are valid open file descriptors.
235        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    // Tier 3 fallback: read/write via std::io::copy (rare  --  procfs, FUSE, etc.)
255    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
266/// Trait for filesystem operations that auto-select the optimal I/O path
267pub trait OptimizedFs {
268    /// Copy an entire file, choosing reflink/sendfile/io_uring as appropriate
269    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    /// Copy a byte range within a file (used for Merkle delta resync)
271    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    /// Truncate a file to the given size
273    fn optimized_truncate(&mut self, dst: PathBuf, size: u64) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
274    /// Rename a file with optional flags (e.g. RENAME_EXCHANGE)
275    fn optimized_rename(&mut self, src: PathBuf, dst: PathBuf, flags: u32) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
276    /// Allocate or punch holes in a file
277    fn optimized_fallocate(&mut self, dst: PathBuf, mode: i32, offset: u64, length: u64) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
278    /// Unlink a file or remove a directory via io_uring UnlinkAt (AT_REMOVEDIR for dirs).
279    /// Falls back to spawn_blocking on EOPNOTSUPP.
280    fn optimized_unlink(&mut self, path: PathBuf, is_dir: bool) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
281    /// Create a hard link via io_uring LinkAt.
282    /// Falls back to spawn_blocking on EOPNOTSUPP.
283    fn optimized_link(&mut self, existing: PathBuf, new: PathBuf) -> impl std::future::Future<Output = Result<CopyStats>> + Send;
284    /// Create a symbolic link via io_uring SymlinkAt.
285    /// Falls back to spawn_blocking on EOPNOTSUPP.
286    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
322/// Resolve a btrfs inode number to its filesystem path via BTRFS_IOC_INO_LOOKUP
323pub fn btrfs_resolve_inode(fd: RawFd, inode: u64) -> Result<PathBuf> {
324    let mut args = BtrfsIoctlInoLookupArgs { treeid: 0, objectid: inode, name: [0; 4080] };
325    // SAFETY: fd is a valid open file descriptor, args is a properly initialized
326    // repr(C) struct matching the kernel's expected layout for BTRFS_IOC_INO_LOOKUP.
327    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    // SAFETY: cname length is checked <= 4039 above, fitting within the
356    // 4040-byte name field. Both pointers are valid and non-overlapping.
357    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    // SAFETY: dest_dir_file is a valid fd, args is a repr(C) struct with
366    // the snapshot name copied in and fd set to the source subvolume.
367    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    // SAFETY: cname length is checked <= 4039 above, fitting within the
392    // 4040-byte name field. Both pointers are valid and non-overlapping.
393    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    // SAFETY: dir_file is a valid fd, args is a repr(C) struct with the
402    // subvolume name written into the name field.
403    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    // SAFETY: BtrfsScrubArgs is repr(C) with no padding requirements
414    // beyond zero-initialization. All-zeros is a valid state.
415    let mut args: BtrfsScrubArgs = unsafe { std::mem::zeroed() };
416    args.devid = 0;
417    
418    // SAFETY: f is a valid fd for the mount point, args is properly initialized.
419    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    // SAFETY: subvol_fd is a valid open fd passed by the caller.
440    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        // SAFETY: fd_dup is a valid duplicated fd, args is a repr(C) struct
460        // with clone_sources pointing to a live Vec (kept alive by the closure).
461        let ret = unsafe { libc::ioctl(fd_dup.as_raw_fd(), BTRFS_IOC_SEND, &mut args) };
462        drop(fd_dup);
463        // SAFETY: pipe_w_fd is a valid raw fd from into_raw_fd().
464        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    // SAFETY: pipe_r is a valid fd from nix::unistd::pipe(). Ownership
474    // transfers to File, which will close it on drop.
475    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    // SAFETY: mount_fd is a valid fd, handle points to a valid FileHandle
482    // struct obtained from name_to_handle_at. The kernel validates the handle.
483    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    // SAFETY: fd is a valid file descriptor (checked >= 0 above).
495    // Ownership transfers to File.
496    Ok(unsafe { std::fs::File::from_raw_fd(fd as i32) })
497}
498
499/// Async copy engine backed by io_uring with auto-adaptive tier selection
500///
501/// Probes source/target capabilities and selects the fastest copy path:
502/// reflink (FICLONE), copy_file_range, sendfile, or pipelined io_uring.
503pub struct SmartCopier {
504    /// io_uring instance for async I/O submission
505    pub ring: IoUring,
506    /// Registered buffer pool for io_uring fixed-buffer reads/writes
507    pub buffer_pool: BufferPool,
508    /// Separate aligned buffer pool for hardware atomic writes (RWF_ATOMIC)
509    pub atomic_buffer_pool: Option<BufferPool>,
510    /// Async wrapper around the io_uring eventfd for tokio integration
511    pub async_fd: Arc<AsyncFd<RawFd>>,
512    /// Enable VDO zero-block optimization (punch holes for all-zero blocks)
513    pub vdo_opt: bool,
514    /// Whether O_DIRECT is safe on the target filesystem
515    pub direct_io_ok: bool,
516    /// Detected capabilities of the source filesystem
517    pub source_caps: Arc<Capabilities>,
518    /// Detected capabilities of the target filesystem
519    pub target_caps: Arc<Capabilities>,
520    /// Number of consecutive zero blocks before triggering VDO stall warning
521    pub vdo_stall_threshold: u32,
522    /// Optional callback invoked after each file copy with the target inode
523    pub barrier_callback: Option<Box<dyn Fn(u64) + Send + Sync>>,
524    /// Use RWF_UNCACHED for source reads
525    pub source_uncached: bool,
526    /// Use RWF_UNCACHED for target writes
527    pub target_uncached: bool,
528    /// PSI-based system stress governor
529    pub governor: Option<Arc<Governor>>,
530    /// Adaptive fsync timeout tracker
531    pub fsync_tracker: FsyncLatencyTracker,
532    /// Skip fsync after writes (for batch operations that fsync at the end)
533    pub skip_fsync: bool,
534    /// Seconds without progress before declaring a segment stall
535    pub segment_stall_timeout_secs: u64,
536    /// Maximum seconds for an entire segment copy before timeout
537    pub segment_overall_timeout_secs: u64,
538    /// Separate io_uring ring for postcopy operations (rename, fsync, unlink, link, symlink, truncate, statx).
539    /// Uses IORING_SETUP_ATTACH_WQ to share the kernel async worker pool with `ring`.
540    pub postcopy_ring: IoUring,
541    /// Async eventfd wrapper for the postcopy ring.
542    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    /// Stat a path via io_uring `Statx` requesting only `STATX_NLINK`, returning the
605    /// hard-link count.  Falls back to `spawn_blocking(std::fs::metadata)` when the
606    /// kernel returns `EOPNOTSUPP` or `EINVAL` (e.g. older kernels, NFS).
607    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        // SAFETY: zero-initialised statx is valid  --  kernel fills requested fields.
612        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
642/// Ensure the parent directory of a target path exists
643pub(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
651/// Full-file sendfile(2) transfer. Returns `CopyStats` on success.
652/// Called from within `spawn_blocking`  --  safe to block.
653///
654/// # Safety
655/// `sfd` and `dfd` must be valid open file descriptors held alive by the caller.
656fn 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        // SAFETY: sfd and dfd are valid open file descriptors held alive by the caller.
662        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
686/// Build a Write SQE targeting either a fixed file slot (`WriteFixed`) or raw fd (`Write`).
687///
688/// When `use_fixed` is true, uses `WriteFixed` with a pre-registered file slot.
689/// When false, falls back to `Write` with a raw fd (no registered buffer index needed).
690///
691/// # Safety
692/// Caller must ensure `ptr` is valid for `len` bytes and the fd/slot is registered
693/// with the ring for the lifetime of the submitted SQE.
694unsafe 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
713/// Attempt NFS sendfile fast-path for large files on NFS targets.
714/// Returns `Ok(Some(stats))` if sendfile succeeded, `Ok(None)` to fall through.
715async 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            // SAFETY: sfd and dfd are valid open fds. Reset positions for fallback.
741            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
750/// Attempt cachestat-guided sendfile fast-path for cached files in the 64KB-1MB range.
751/// Returns `Ok(Some(stats))` if sendfile succeeded, `Ok(None)` to fall through.
752async 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            // SAFETY: resetting file positions for io_uring fallback.
789            unsafe { libc::lseek(sfd, 0, libc::SEEK_SET); libc::lseek(dfd, 0, libc::SEEK_SET); };
790            Ok(None)
791        }
792    }
793}
794
795/// Submit an io_uring fsync with adaptive timeout and blocking fallback.
796/// When `skip_fsync` is true, only flushes pending submissions without fsync.
797async 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        // SAFETY: the SQE is fully built with valid fd references.
820        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                    // SAFETY: reading 8 bytes from the eventfd to drain readiness notification.
830                    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                        // drain
837                    }
838                }
839            }
840        }
841    }
842
843    if !pushed {
844         let _ = ring.submit();
845         // SAFETY: the SQE is fully built with valid fd references.
846         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              // SAFETY: dfd is a valid open file descriptor.
872             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                // SAFETY: reading 8 bytes from the eventfd to drain readiness notification.
880                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                    // drain
888                }
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
904/// Handle a completed READ_OP CQE: detect zero blocks and emit the appropriate
905/// write or hole-punch SQE. Returns `(bytes_zeros_delta, bytes_processed_delta)` on
906/// success (a SQE was pushed). On error the buffer is released internally.
907fn 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        // SAFETY: ptr is from buffer_pool.get_ptr() (valid allocation),
935        // and bytes_transferred <= buffer capacity (set by io_uring completion).
936        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        // SAFETY: the SQE is fully built with valid fd references.
955        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    // SAFETY: ptr is valid for bytes_transferred from the pool; fd/slot registered with ring.
965    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    // SAFETY: the SQE is fully built with valid fd and buffer references.
968    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        // SAFETY: fd is a valid open file descriptor. POSIX_FADV_SEQUENTIAL
992        // doubles the kernel readahead window; no-op on O_DIRECT or unsupported FS.
993        // posix_fadvise returns error code directly (not via errno).
994        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        // SAFETY: fd is a valid open file descriptor. The ioctl sets/clears
1006        // F2FS pinning; failure is non-fatal (ignored with let _).
1007        let _ = unsafe { libc::ioctl(fd, F2FS_IOC_SET_PIN_FILE, &val) };
1008    }
1009
1010    /// Copy a file (or range) from src to dst using the full io_uring pipeline
1011    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    /// Copy with an optional buffer count limit for backpressure control
1043    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    /// Dispatch a filesystem operation (copy, rename, truncate, fallocate)
1101    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    /// Probe the source file's sparse layout using `SEEK_HOLE` / `SEEK_DATA`.
1242    /// Returns the hole ratio (0.0 = dense, 1.0 = fully sparse) AND the full
1243    /// segment map, so callers can avoid a second walk via `map_sparse_segments`.
1244    /// Returns `None` if the filesystem doesn't support `SEEK_HOLE` or probing fails.
1245    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            // SAFETY: src_fd is a valid open file descriptor; SEEK_DATA is a standard lseek whence.
1256            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                        // Rest of file is a hole
1262                        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            // SAFETY: src_fd is a valid open file descriptor; SEEK_HOLE is a standard lseek whence.
1285            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        // SAFETY: restoring seek position to start after probing.
1303        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            // SAFETY: fd is a valid open file descriptor on an F2FS filesystem.
1340            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        // Skip SEEK_HOLE/SEEK_DATA probe for NFS targets  --  each lseek is a
1346        // synchronous RPC and NFS doesn't track holes locally.
1347        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        // Skip fallocate(mode=0) for NFS targets  --  sendfile extends the file
1353        // naturally, and fallocate is a synchronous ALLOCATE RPC on NFS that
1354        // pre-allocates blocks wastefully.
1355        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            // SAFETY: fd is a valid open file descriptor. fallocate with
1364            // mode 0 pre-allocates space without modifying file contents.
1365            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    /// Submit a single SQE to the io_uring and wait for its CQE via the ring's async eventfd.
1378    /// Returns the CQE result value (>= 0 on success). Errors on negative (kernel errno).
1379    /// Used for post-copy operations (fsync, rename) that don't fit the pipelined model.
1380    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        // SAFETY: the SQE is fully built by the caller with valid fd and buffer references.
1386        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                    // SAFETY: reading 8 bytes from the eventfd to drain readiness notification.
1407                    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                    // Timeout  --  resubmit and check completion again
1413                    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                // Atomic rename: exchange path stays spawn_blocking, simple path uses
1463                // io_uring with EBADF fallback to spawn_blocking
1464                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            // register_files failed on a non-NFS local target — skip io_uring to avoid
1597            // per-segment stalls (up to PROCESS_SEGMENT_STALL_SECS each).
1598            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                    // SAFETY: sfd and dfd are valid open file descriptors.
1610                    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                // SAFETY: sfd and dfd are valid. Seek to range start for sendfile fallback.
1632                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                    // SAFETY: sfd and dfd are valid open file descriptors.
1639                    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                                // SAFETY: sfd and dfd are valid open file descriptors.
1691                                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                            // SAFETY: sfd is valid. Seek to range start for sendfile fallback.
1713                            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                                // SAFETY: sfd and dfd are valid open file descriptors.
1720                                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                // SAFETY: sfd and dfd are valid open file descriptors.
1833                // Resetting file positions to 0 before fallback copy.
1834                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        // Local sendfile fast-path: for non-NFS, non-sparse targets, sendfile is faster
1853        // than io_uring (avoids 4KB buffer chunking overhead) and avoids EBADF on kernels
1854        // where register_files succeeds but fixed-fd operations fail (e.g. btrfs 7.3.0-rc0).
1855        // io_uring is only used for NFS (async pipelining) and sparse files (hole-aware I/O).
1856        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                        // SAFETY: sfd and dfd are valid open file descriptors held
1896                        // alive by sf and _file_handle in the calling scope.
1897                        // Reset positions and truncate destination for clean retry.
1898                        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                            // SAFETY: dfd is valid. Re-allocate space for non-sparse retry.
1905                            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                    // SAFETY: dfd is a valid fd with an active F2FS atomic write transaction.
1938                    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                    // SAFETY: dfd is a valid fd. Aborting the atomic transaction on error.
1946                    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        // Post-copy fsync: reuse the already-open dfd instead of re-opening the temp file.
1962        // Saves 1 OPEN + 1 CLOSE RPC on NFS, 2 syscalls on local filesystems.
1963        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                        // SAFETY: dfd is a valid open fd from prepare_destination_file.
1974                        unsafe { libc::fsync(dfd) };
1975                    }
1976                    Err(e) => return Ok(Err(e)),
1977                }
1978            }
1979
1980        // Post-copy FADV_DONTNEED on source: release page cache after copy completes.
1981        // Best-effort  --  any error is logged and ignored.
1982        {
1983            let fadv_sqe = opcode::Fadvise::new(types::Fd(sfd), 0, libc::POSIX_FADV_DONTNEED)
1984                .build()
1985                .user_data(FADVISE_OP);
1986            // SAFETY: the SQE is fully built with a valid fd.
1987            if unsafe { ring.submission().push(&fadv_sqe) }.is_ok() {
1988                let _ = ring.submit();
1989            }
1990            // Fire-and-forget: we do NOT wait for the Fadvise completion.
1991        }
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            // SAFETY: sfd and dfd are valid open file descriptors.
2002            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                    // SAFETY: sfd/dfd are valid fds. off_in/off_out are valid
2025                    // mutable references to offsets updated by the kernel.
2026                    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            // SAFETY: dfd is a valid fd. args is a repr(C) FileCloneRange
2059            // struct with valid source fd, offset, and length.
2060            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            // SAFETY: sfd is a valid open file descriptor.
2117            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            // SAFETY: sfd is a valid open file descriptor.
2148            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                                // SAFETY: the SQE is fully built with valid fd and buffer references.
2236                                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                                            // SAFETY: reading 8 bytes from the eventfd to drain readiness notification.
2246                                            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                                                // drain
2253                                            }
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            // Early exit: error already encountered and nothing in flight to drain.
2363            // Without this, the loop spins for segment_overall_timeout_secs (60s)
2364            // waiting for progress that can never happen (e.g. EBADF from io_uring).
2365            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                 // SAFETY: the SQE is fully built with valid fd and buffer references.
2470                 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                                // SAFETY: reading 8 bytes from the eventfd to drain readiness notification.
2512                                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                                // SAFETY: buf_ptr is valid for retry_len bytes from the pool; fd/slot registered with ring.
2580                                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                                // SAFETY: the SQE is fully built with valid fd and buffer references.
2583                                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                    // Only decrement for ops that were tracked in inflight_ops.
2605                    // FADVISE_OP is fire-and-forget (never incremented inflight_ops).
2606                    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                    // Only decrement for ops that were tracked in inflight_ops.
2617                    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                        // Do NOT decrement inflight_ops — unknown ops were never
2657                        // incremented. Decrementing causes u64 underflow to MAX,
2658                        // which hangs the loop for 60s waiting for completions.
2659                    }
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
2679/// Apply or release a flock on a file, tracking the lock handle by inode in `lock_map`
2680pub 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    // SAFETY: fd is a valid open file descriptor. LOCK_NB makes the
2690    // call non-blocking so it cannot deadlock.
2691    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
2699// ---------------------------------------------------------------------------
2700// Delta copy  --  transfer only dirty ranges identified by Merkle tree diff
2701// ---------------------------------------------------------------------------
2702impl SmartCopier {
2703    /// Copy only the specified dirty ranges from src to dst.
2704    /// Each DirtyRange triggers an `optimized_copy_range()` call.
2705    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        // Use /var/tmp (real filesystem) instead of /tmp (tmpfs)  --  io_uring
2755        // intermittently returns ENOENT on tmpfs under parallel test load.
2756        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        // is_sparse=true: fallocate should be skipped
2857        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        // With is_sparse=true, no fallocate -> minimal blocks allocated
2865        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        // is_sparse=false: fallocate should be called
2876        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        // On filesystems supporting fallocate, blocks should be pre-allocated.
2884        // 65536 bytes / 512 bytes per block = 128 blocks minimum.
2885        // Some FS may report more due to alignment. On tmpfs fallocate may not
2886        // allocate blocks, so we just verify the file was created successfully.
2887        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        // Tests that inflight_ops reaches 0 after a copy completes.
2894        // If inflight_ops underflows or leaks, the copy loop hangs until timeout.
2895        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        // Use a size that exercises multiple io_uring chunks (>64KB)
2901        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        // If inflight_ops doesn't reach 0, this will hang until the stall timeout
2908        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        // Verify byte-identical copy (proves the loop completed correctly)
2922        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        // Regression test for the _ => arm bug in process_data_segment.
2930        // Before the fix: unknown CQE op types caused inflight_ops to never reach 0,
2931        // hanging the loop for 60 seconds (600 * 100ms timeouts).
2932        // After the fix: unknown op types decrement inflight_ops and release the buffer.
2933        //
2934        // This test verifies the copy pipeline completes within a tight timeout.
2935        // If the _ => arm bug were triggered, this would timeout at 10s instead of 60s.
2936        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        // 150KB — exercises io_uring Tier 3 path (>64KB)
2942        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        // 10-second timeout: well above normal copy time (~100ms), well below the
2950        // 60-second hang that the _ => arm bug would cause.
2951        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        // Given: a temp file with known content and an io_uring ring WITHOUT register_files
3015        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        // Deliberately NOT calling register_files — simulates the use_fixed=false path
3036
3037        // When: submitting Read(Fd) then Write(Fd) opcodes with raw fds
3038        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        // Then: Read succeeds (no EBADF)
3049        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        // Then: Write succeeds (no EBADF)
3066        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        // Then: written content matches source
3074        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        // Verify byte-for-byte content match after reflink_or_copy
3081        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        // Zero-byte files must be handled without error
3095        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        // Missing source must return an error, not panic
3107        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        // On XFS with reflink=1, verify FICLONE fires (shared extents).
3117        // Skip if /var/tmp is not XFS or doesn't support reflink.
3118        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        // 64KB — above the 4096-byte threshold in determine_copy_strategy
3126        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        // Verify content
3131        assert_eq!(std::fs::read(&dst).unwrap(), data);
3132        // Check if filefrag shows shared extents (XFS reflink=1 only)
3133        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                // FICLONE fired — shared extents confirmed
3138                println!("FICLONE confirmed: shared extents detected");
3139            }
3140            // If no "shared" — FICLONE fell back to sendfile (cross-device or non-reflink FS)
3141            // This is acceptable — the test verifies content correctness regardless
3142        }
3143    }
3144}