Skip to main content

acyclic_fs/kernel/
extent_clone.rs

1//! Zero-copy logical range clone over immutable sparse extents.
2
3use super::{
4    DecodeLimits, ExtentMutation, ExtentMutationError, ExtentMutationOptions,
5    apply_extent_mutations_async, plan_extent_range_async,
6};
7use crate::cancellation::CancellationToken;
8use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
9use crate::storage::{ByteRange, ObjectId, ObjectKind};
10use std::mem::size_of;
11use thiserror::Error;
12
13/// Candidate destination extent root and exact clone work.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct ExtentCloneReceipt {
16    /// Candidate immutable destination extent root.
17    pub root: ObjectId,
18    /// Destination logical size after extension, if any.
19    pub logical_bytes: u64,
20    /// Exact source planning, interval compilation, and destination path-copy work.
21    pub work: WorkCounters,
22}
23
24/// Range clone failure retaining all completed work and safe orphan writes.
25pub type ExtentCloneFailure = OperationFailure<ExtentCloneError>;
26
27/// Complete bounded source and destination range-clone request.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct ExtentCloneRequest {
30    /// Authenticated source extent root.
31    pub source_root: ObjectId,
32    /// Exact source logical file size.
33    pub source_logical_bytes: u64,
34    /// Non-empty source range.
35    pub source_range: ByteRange,
36    /// Authenticated destination extent root.
37    pub destination_root: ObjectId,
38    /// Exact destination logical file size before clone.
39    pub destination_logical_bytes: u64,
40    /// Inclusive destination start offset.
41    pub destination_offset: u64,
42    /// Maximum source sparse spans admitted in the clone plan.
43    pub maximum_spans: u32,
44    /// Maximum destination interval mutations admitted.
45    pub maximum_mutations: u32,
46}
47
48/// Clones one logical sparse range without reading or copying content bytes.
49///
50/// The source range is fully planned before any destination object is written,
51/// so overlapping same-file clone has snapshot semantics. Hole, allocated-zero,
52/// and content representations remain distinct. Content spans reuse immutable
53/// blob identities and adjusted offsets.
54///
55/// # Errors
56///
57/// Rejects wrong object classes, empty/overflowing ranges, insufficient span or
58/// mutation bounds, malformed source/destination extent trees, storage errors,
59/// and work outside the admitted budget.
60pub fn clone_extent_range<S: crate::ImmediateObjectStore>(
61    store: &S,
62    request: ExtentCloneRequest,
63    limits: DecodeLimits,
64    budget: WorkBudget,
65) -> Result<ExtentCloneReceipt, ExtentCloneFailure> {
66    let cancellation = CancellationToken::new();
67    crate::async_storage::poll_immediate(clone_extent_range_async(
68        store,
69        request,
70        limits,
71        budget,
72        &cancellation,
73    ))
74}
75
76/// Asynchronously clones a sparse logical range through the canonical planner
77/// and destination path-copy machine.
78///
79/// # Errors
80///
81/// Returns the same bounded failures as [`clone_extent_range`], including
82/// cooperative cancellation before either source reads or destination writes.
83pub async fn clone_extent_range_async<S: crate::AsyncObjectStore>(
84    store: &S,
85    request: ExtentCloneRequest,
86    limits: DecodeLimits,
87    budget: WorkBudget,
88    cancellation: &CancellationToken,
89) -> Result<ExtentCloneReceipt, ExtentCloneFailure> {
90    validate_request(request, limits)?;
91    let plan = plan_extent_range_async(
92        store,
93        super::ExtentRangeRequest {
94            root: request.source_root,
95            file_size: request.source_logical_bytes,
96            range: request.source_range,
97            maximum_spans: request.maximum_spans,
98            limits,
99            budget,
100        },
101        cancellation,
102    )
103    .await
104    .map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
105    let allocation_bytes = u64::try_from(plan.spans.len())
106        .unwrap_or(u64::MAX)
107        .saturating_mul(u64::try_from(size_of::<ExtentMutation>()).unwrap_or(u64::MAX));
108    let simultaneous_plan_allocation = plan
109        .retained_allocation_bytes
110        .checked_add(allocation_bytes)
111        .ok_or_else(|| {
112            OperationFailure::new(ExtentCloneError::Work(WorkError::Overflow), plan.work)
113        })?;
114    let mut work = plan
115        .work
116        .checked_add(WorkCounters {
117            allocation_operations: 1,
118            items_examined: u64::try_from(plan.spans.len()).unwrap_or(u64::MAX),
119            ..WorkCounters::default()
120        })
121        .map_err(|error| OperationFailure::new(error.into(), plan.work))?;
122    work.peak_allocation_bytes = work.peak_allocation_bytes.max(simultaneous_plan_allocation);
123    work.verify(budget)
124        .map_err(|error| OperationFailure::new(error.into(), plan.work))?;
125    let mut mutations = Vec::new();
126    if mutations.try_reserve_exact(plan.spans.len()).is_err() {
127        return Err(OperationFailure::new(
128            ExtentCloneError::AllocationFailed,
129            work,
130        ));
131    }
132    for span in plan.spans {
133        let relative = span
134            .offset
135            .checked_sub(request.source_range.offset)
136            .ok_or_else(|| OperationFailure::new(ExtentCloneError::RangeOverflow, work))?;
137        let offset = request
138            .destination_offset
139            .checked_add(relative)
140            .ok_or_else(|| OperationFailure::new(ExtentCloneError::RangeOverflow, work))?;
141        mutations.push(ExtentMutation::Replace {
142            offset,
143            length: span.length,
144            kind: span.kind,
145            extend: true,
146        });
147    }
148    let mut remaining = work
149        .remaining(budget)
150        .map_err(|error| OperationFailure::new(error.into(), work))?;
151    remaining.peak_allocation_bytes = budget
152        .peak_allocation_bytes
153        .checked_sub(allocation_bytes)
154        .ok_or_else(|| OperationFailure::new(ExtentCloneError::Work(WorkError::Overflow), work))?;
155    let receipt = apply_extent_mutations_async(
156        store,
157        request.destination_root,
158        request.destination_logical_bytes,
159        &mutations,
160        ExtentMutationOptions {
161            maximum_mutations: request.maximum_mutations,
162            limits,
163            budget: remaining,
164        },
165        cancellation,
166    )
167    .await
168    .map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
169    let destination_peak = allocation_bytes
170        .checked_add(receipt.work.peak_allocation_bytes)
171        .ok_or_else(|| OperationFailure::new(ExtentCloneError::Work(WorkError::Overflow), work))?;
172    let mut destination_work = receipt.work;
173    destination_work.peak_allocation_bytes = 0;
174    work = work
175        .checked_add(destination_work)
176        .map_err(|error| OperationFailure::new(error.into(), work))?;
177    work.peak_allocation_bytes = work.peak_allocation_bytes.max(destination_peak);
178    work.verify(budget)
179        .map_err(|error| OperationFailure::new(error.into(), work))?;
180    Ok(ExtentCloneReceipt {
181        root: receipt.root,
182        logical_bytes: receipt.logical_bytes,
183        work,
184    })
185}
186
187fn validate_request(
188    request: ExtentCloneRequest,
189    limits: DecodeLimits,
190) -> Result<(), ExtentCloneFailure> {
191    let error = if request.source_root.kind != ObjectKind::ExtentPage
192        || request.destination_root.kind != ObjectKind::ExtentPage
193    {
194        Some(ExtentCloneError::WrongRootKind)
195    } else if request.source_range.length == 0 {
196        Some(ExtentCloneError::EmptyRange)
197    } else if request
198        .source_range
199        .offset
200        .checked_add(request.source_range.length)
201        .is_none()
202        || request
203            .destination_offset
204            .checked_add(request.source_range.length)
205            .is_none()
206    {
207        Some(ExtentCloneError::RangeOverflow)
208    } else if request.maximum_spans == 0
209        || request.maximum_mutations < request.maximum_spans
210        || !limits.page_limits_valid(2)
211    {
212        Some(ExtentCloneError::InvalidLimits)
213    } else {
214        None
215    };
216    match error {
217        Some(error) => Err(OperationFailure::before_work(error)),
218        None => Ok(()),
219    }
220}
221
222/// Sparse range-clone failures.
223#[derive(Debug, Error)]
224pub enum ExtentCloneError {
225    /// Source or destination is not an extent-page object.
226    #[error("range clone root has the wrong object kind")]
227    WrongRootKind,
228    /// Clone length must be positive.
229    #[error("range clone is empty")]
230    EmptyRange,
231    /// Source or destination range arithmetic overflowed.
232    #[error("range clone overflowed")]
233    RangeOverflow,
234    /// Span/mutation limits cannot admit the operation.
235    #[error("range clone limits are invalid")]
236    InvalidLimits,
237    /// The admitted clone-plan scratch allocation was unavailable.
238    #[error("range clone scratch allocation failed")]
239    AllocationFailed,
240    /// Source sparse-range planning failed.
241    #[error(transparent)]
242    Source(#[from] super::ExtentReadError),
243    /// Destination sparse mutation failed.
244    #[error(transparent)]
245    Destination(#[from] ExtentMutationError),
246    /// Exact work exceeded or overflowed its budget.
247    #[error(transparent)]
248    Work(#[from] WorkError),
249}
250
251#[cfg(all(test, feature = "memory"))]
252#[path = "tests/extent_clone.rs"]
253mod tests;