Skip to main content

acyclic_fs/kernel/
rebase.rs

1//! Exact observation and mutation dependencies for no-scan safe rebasing.
2
3use super::{ExtentSeekTarget, LogicalName};
4use crate::cancellation::CancellationToken;
5use crate::foundation::{Digest, FileId, GenerationId};
6use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
7use std::collections::BTreeMap;
8use std::future::Future;
9use thiserror::Error;
10
11/// One exact semantic region whose state can affect a checkout.
12#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
13pub enum DependencyRegion {
14    /// Complete path-independent file record, including kind and payload roots.
15    FileRecord(FileId),
16    /// Complete canonical metadata object selected by one file record.
17    Metadata(FileId),
18    /// Exact logical length of one regular file.
19    FileLength(FileId),
20    /// A non-empty bounded logical content range.
21    ContentRange {
22        /// Stable file identity.
23        file_id: FileId,
24        /// Inclusive logical offset.
25        offset: u64,
26        /// Positive byte length.
27        length: u64,
28    },
29    /// One exact sparse seek observation.
30    SparseSeek {
31        /// Stable file identity.
32        file_id: FileId,
33        /// Inclusive logical starting offset.
34        offset: u64,
35        /// Sparse class requested by the caller.
36        target: ExtentSeekTarget,
37    },
38    /// One exact name lookup, including a negative lookup when state is absent.
39    DirectoryName {
40        /// Stable directory identity.
41        directory_id: FileId,
42        /// Canonical queried name.
43        name: LogicalName,
44    },
45    /// A bounded directory cursor interval used to prevent phantom-safe rebases.
46    DirectoryRange {
47        /// Stable directory identity.
48        directory_id: FileId,
49        /// Exclusive lower cursor; `None` starts before the first name.
50        after: Option<LogicalName>,
51        /// Positive maximum entries requested by the observed page.
52        maximum_entries: u32,
53    },
54}
55
56impl DependencyRegion {
57    /// Validates range arithmetic and directory cursor ordering.
58    ///
59    /// # Errors
60    ///
61    /// Rejects empty/overflowing content ranges and inverted directory windows.
62    pub fn validate(&self) -> Result<(), DependencyError> {
63        match self {
64            Self::ContentRange { offset, length, .. } => {
65                if *length == 0 {
66                    return Err(DependencyError::EmptyContentRange);
67                }
68                offset
69                    .checked_add(*length)
70                    .ok_or(DependencyError::RangeOverflow)?;
71            }
72            Self::DirectoryRange {
73                maximum_entries: 0, ..
74            } => return Err(DependencyError::ZeroDirectoryPageLimit),
75            Self::FileRecord(_)
76            | Self::Metadata(_)
77            | Self::FileLength(_)
78            | Self::SparseSeek { .. }
79            | Self::DirectoryName { .. }
80            | Self::DirectoryRange { .. } => {}
81        }
82        Ok(())
83    }
84}
85
86/// Canonical semantic state of one exact dependency region.
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum DependencyState {
89    /// The exact queried name/record/region does not exist.
90    Absent,
91    /// Domain-separated digest of the complete bounded semantic result.
92    Present(Digest),
93}
94
95/// One dependency captured while evaluating or mutating a checkout.
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct Dependency {
98    /// Exact semantic region.
99    pub region: DependencyRegion,
100    /// State authenticated against the checkout's base generation.
101    pub expected: DependencyState,
102}
103
104/// Whether a conflicting dependency came from observation, mutation, or both.
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106pub enum DependencyUse {
107    /// A read, stat, listing, or negative lookup observed this region.
108    Observation,
109    /// A local mutation uses this region as its compare-before-write base.
110    Mutation,
111    /// The same region was both observed and mutated.
112    ObservationAndMutation,
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116struct CapturedState {
117    expected: DependencyState,
118    usage: DependencyUse,
119}
120
121/// Validated, sorted, and deduplicated checkout dependency proof.
122///
123/// Construction pays normalization cost once. Equal-generation refresh can
124/// therefore return in constant work regardless of dependency count.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct CheckoutDependencies {
127    captured: BTreeMap<DependencyRegion, CapturedState>,
128}
129
130impl CheckoutDependencies {
131    /// Validates and normalizes exact observations and mutation preconditions.
132    ///
133    /// # Errors
134    ///
135    /// Rejects excessive, malformed, or contradictory dependencies.
136    pub fn new(
137        observations: impl IntoIterator<Item = Dependency>,
138        mutations: impl IntoIterator<Item = Dependency>,
139        maximum_dependencies: u32,
140    ) -> Result<Self, DependencyError> {
141        if maximum_dependencies == 0 {
142            return Err(DependencyError::ZeroLimit);
143        }
144        let mut captured: BTreeMap<DependencyRegion, CapturedState> = BTreeMap::new();
145        for (dependency, usage) in observations
146            .into_iter()
147            .map(|value| (value, DependencyUse::Observation))
148            .chain(
149                mutations
150                    .into_iter()
151                    .map(|value| (value, DependencyUse::Mutation)),
152            )
153        {
154            dependency.region.validate()?;
155            if let Some(existing) = captured.get_mut(&dependency.region) {
156                if existing.expected != dependency.expected {
157                    return Err(DependencyError::ContradictoryState);
158                }
159                if existing.usage != usage {
160                    existing.usage = DependencyUse::ObservationAndMutation;
161                }
162                continue;
163            }
164            if u32::try_from(captured.len()).unwrap_or(u32::MAX) >= maximum_dependencies {
165                return Err(DependencyError::TooManyDependencies {
166                    maximum: maximum_dependencies,
167                });
168            }
169            captured.insert(
170                dependency.region,
171                CapturedState {
172                    expected: dependency.expected,
173                    usage,
174                },
175            );
176        }
177        Ok(Self { captured })
178    }
179
180    /// Number of distinct exact regions in the proof.
181    #[must_use]
182    pub fn len(&self) -> usize {
183        self.captured.len()
184    }
185
186    /// Whether no operation has observed or mutated a region.
187    #[must_use]
188    pub fn is_empty(&self) -> bool {
189        self.captured.is_empty()
190    }
191
192    pub(crate) fn clear(&mut self) {
193        self.captured.clear();
194    }
195
196    pub(crate) fn clear_mutations(&mut self) {
197        self.captured.retain(|_, state| match state.usage {
198            DependencyUse::Observation => true,
199            DependencyUse::Mutation => false,
200            DependencyUse::ObservationAndMutation => {
201                state.usage = DependencyUse::Observation;
202                true
203            }
204        });
205    }
206
207    pub(crate) fn extend_observations(
208        &mut self,
209        dependencies: Vec<Dependency>,
210        maximum_dependencies: u32,
211    ) -> Result<(), DependencyError> {
212        self.extend(
213            dependencies,
214            maximum_dependencies,
215            DependencyUse::Observation,
216        )
217    }
218
219    pub(crate) fn extend_mutations(
220        &mut self,
221        dependencies: Vec<Dependency>,
222        maximum_dependencies: u32,
223    ) -> Result<(), DependencyError> {
224        self.extend(dependencies, maximum_dependencies, DependencyUse::Mutation)
225    }
226
227    fn extend(
228        &mut self,
229        dependencies: Vec<Dependency>,
230        maximum_dependencies: u32,
231        usage: DependencyUse,
232    ) -> Result<(), DependencyError> {
233        if maximum_dependencies == 0 {
234            return Err(DependencyError::ZeroLimit);
235        }
236        let mut staged = BTreeMap::new();
237        for dependency in dependencies {
238            dependency.region.validate()?;
239            if let Some(existing) = self.captured.get(&dependency.region)
240                && existing.expected != dependency.expected
241            {
242                return Err(DependencyError::ContradictoryState);
243            }
244            match staged.get(&dependency.region) {
245                Some(expected) if *expected != dependency.expected => {
246                    return Err(DependencyError::ContradictoryState);
247                }
248                Some(_) => {}
249                None => {
250                    staged.insert(dependency.region, dependency.expected);
251                }
252            }
253        }
254        let distinct_new = staged
255            .keys()
256            .filter(|region| !self.captured.contains_key(*region))
257            .count();
258        if self.captured.len().saturating_add(distinct_new)
259            > usize::try_from(maximum_dependencies).unwrap_or(usize::MAX)
260        {
261            return Err(DependencyError::TooManyDependencies {
262                maximum: maximum_dependencies,
263            });
264        }
265        for (region, expected) in staged {
266            match self.captured.get_mut(&region) {
267                Some(existing) => {
268                    if existing.usage != usage
269                        && existing.usage != DependencyUse::ObservationAndMutation
270                    {
271                        existing.usage = DependencyUse::ObservationAndMutation;
272                    }
273                }
274                None => {
275                    self.captured
276                        .insert(region, CapturedState { expected, usage });
277                }
278            }
279        }
280        Ok(())
281    }
282}
283
284/// Backend-independent exact-region resolver.
285///
286/// Implementations must derive `DependencyState` from canonical authenticated
287/// generation objects, not from mutable projections or cache presence.
288pub trait RebaseProbe {
289    /// Stable backend-specific failure.
290    type Error: std::error::Error;
291
292    /// Resolves one exact semantic region in one immutable generation.
293    ///
294    /// # Errors
295    ///
296    /// Returns a backend-specific failure with exact work performed before the
297    /// region could be authenticated.
298    fn probe(
299        &self,
300        generation: GenerationId,
301        region: &DependencyRegion,
302        budget: WorkBudget,
303    ) -> Result<ProbeReceipt, OperationFailure<Self::Error>>;
304}
305
306/// Runtime-neutral nonblocking exact-region resolver.
307pub trait AsyncRebaseProbe {
308    /// Stable backend-specific failure.
309    type Error: std::error::Error;
310
311    /// Resolves one exact semantic region without blocking the calling runtime.
312    fn probe_async(
313        &self,
314        generation: GenerationId,
315        region: &DependencyRegion,
316        budget: WorkBudget,
317        cancellation: &CancellationToken,
318    ) -> impl Future<Output = Result<ProbeReceipt, OperationFailure<Self::Error>>>;
319}
320
321/// One exact-region result and the work required to authenticate it.
322#[derive(Clone, Debug, Eq, PartialEq)]
323pub struct ProbeReceipt {
324    /// Canonical semantic state in the selected generation.
325    pub value: DependencyState,
326    /// Exact backend and semantic work.
327    pub work: WorkCounters,
328}
329
330/// One region-specific reason automatic advancement is unsafe.
331#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct RebaseConflict {
333    /// Exact changed region.
334    pub region: DependencyRegion,
335    /// How the checkout depended on it.
336    pub usage: DependencyUse,
337    /// State captured from the old base.
338    pub expected: DependencyState,
339    /// State resolved from the candidate generation.
340    pub actual: DependencyState,
341}
342
343/// Bounded safe-rebase result.
344#[derive(Clone, Debug, Eq, PartialEq)]
345pub enum RebaseDecision {
346    /// Candidate generation can become the new base without replay ambiguity.
347    Safe {
348        /// New immutable base.
349        generation: GenerationId,
350    },
351    /// Candidate changed one or more exact dependencies.
352    Conflicted {
353        /// Region-specific conflicts, bounded by the caller.
354        conflicts: Vec<RebaseConflict>,
355        /// More conflicts existed than the caller admitted returning.
356        truncated: bool,
357    },
358}
359
360/// Successful decision with exact probe work.
361#[derive(Clone, Debug, Eq, PartialEq)]
362pub struct RebaseReceipt {
363    /// Safe or region-conflicted result.
364    pub decision: RebaseDecision,
365    /// Exact semantic and backend work.
366    pub work: WorkCounters,
367}
368
369/// Safe-rebase classification failures.
370#[derive(Debug, Error)]
371pub enum RebaseError<E: std::error::Error> {
372    /// Cooperative cancellation occurred before the next region probe.
373    #[error("safe rebase classification was cancelled")]
374    Cancelled,
375    /// Conflict output must have a positive hard bound.
376    #[error("maximum rebase conflicts must be non-zero")]
377    ZeroConflictLimit,
378    /// Exact region resolution failed.
379    #[error("rebase region probe failed: {0}")]
380    Probe(E),
381    /// Work exceeded or overflowed the admitted budget.
382    #[error(transparent)]
383    Work(#[from] WorkError),
384}
385
386/// Compares only captured regions against a candidate generation.
387///
388/// Equal roots return with zero work and no probes. Otherwise each distinct
389/// dependency is resolved once; untouched volume regions are never enumerated.
390///
391/// # Errors
392///
393/// Fails with retained work when conflict bounds, probe execution, or work
394/// admission fail.
395pub fn classify_rebase<P: RebaseProbe>(
396    probe: &P,
397    base: GenerationId,
398    candidate: GenerationId,
399    dependencies: &CheckoutDependencies,
400    maximum_conflicts: u32,
401    budget: WorkBudget,
402) -> Result<RebaseReceipt, OperationFailure<RebaseError<P::Error>>> {
403    if base == candidate {
404        return Ok(RebaseReceipt {
405            decision: RebaseDecision::Safe {
406                generation: candidate,
407            },
408            work: WorkCounters::default(),
409        });
410    }
411    if maximum_conflicts == 0 {
412        return Err(OperationFailure::before_work(
413            RebaseError::ZeroConflictLimit,
414        ));
415    }
416    let mut work = WorkCounters::default();
417    let mut conflicts = Vec::new();
418    let mut truncated = false;
419    for (region, captured) in &dependencies.captured {
420        let semantic = WorkCounters {
421            items_examined: 1,
422            ..WorkCounters::default()
423        };
424        work = work
425            .checked_add(semantic)
426            .map_err(|error| OperationFailure::new(error.into(), work))?;
427        let remaining = work
428            .remaining(budget)
429            .map_err(|error| OperationFailure::new(error.into(), work))?;
430        let actual = probe
431            .probe(candidate, region, remaining)
432            .map_err(|failure| failure.map_with_prior_work(work, RebaseError::Probe))?;
433        work = work
434            .checked_add(actual.work)
435            .map_err(|error| OperationFailure::new(error.into(), work))?;
436        work.verify(budget)
437            .map_err(|error| OperationFailure::new(error.into(), work))?;
438        if actual.value != captured.expected {
439            if u32::try_from(conflicts.len()).unwrap_or(u32::MAX) < maximum_conflicts {
440                conflicts.push(RebaseConflict {
441                    region: region.clone(),
442                    usage: captured.usage,
443                    expected: captured.expected,
444                    actual: actual.value,
445                });
446            } else {
447                truncated = true;
448            }
449        }
450    }
451    let decision = if conflicts.is_empty() {
452        RebaseDecision::Safe {
453            generation: candidate,
454        }
455    } else {
456        RebaseDecision::Conflicted {
457            conflicts,
458            truncated,
459        }
460    };
461    Ok(RebaseReceipt { decision, work })
462}
463
464/// Nonblocking safe-rebase classification over the same exact dependencies as
465/// [`classify_rebase`].
466///
467/// # Errors
468///
469/// Fails with retained exact work on cancellation, probe failure, invalid
470/// conflict bounds, or work-budget exhaustion.
471pub async fn classify_rebase_async<P: AsyncRebaseProbe>(
472    probe: &P,
473    base: GenerationId,
474    candidate: GenerationId,
475    dependencies: &CheckoutDependencies,
476    maximum_conflicts: u32,
477    budget: WorkBudget,
478    cancellation: &CancellationToken,
479) -> Result<RebaseReceipt, OperationFailure<RebaseError<P::Error>>> {
480    if base == candidate {
481        return Ok(RebaseReceipt {
482            decision: RebaseDecision::Safe {
483                generation: candidate,
484            },
485            work: WorkCounters::default(),
486        });
487    }
488    if maximum_conflicts == 0 {
489        return Err(OperationFailure::before_work(
490            RebaseError::ZeroConflictLimit,
491        ));
492    }
493    let mut work = WorkCounters::default();
494    let mut conflicts = Vec::new();
495    let mut truncated = false;
496    for (region, captured) in &dependencies.captured {
497        cancellation
498            .check()
499            .map_err(|_| OperationFailure::new(RebaseError::Cancelled, work))?;
500        work = work
501            .checked_add(WorkCounters {
502                items_examined: 1,
503                ..WorkCounters::default()
504            })
505            .map_err(|error| OperationFailure::new(error.into(), work))?;
506        let remaining = work
507            .remaining(budget)
508            .map_err(|error| OperationFailure::new(error.into(), work))?;
509        let actual = probe
510            .probe_async(candidate, region, remaining, cancellation)
511            .await
512            .map_err(|failure| failure.map_with_prior_work(work, RebaseError::Probe))?;
513        work = work
514            .checked_add(actual.work)
515            .map_err(|error| OperationFailure::new(error.into(), work))?;
516        work.verify(budget)
517            .map_err(|error| OperationFailure::new(error.into(), work))?;
518        if actual.value != captured.expected {
519            if u32::try_from(conflicts.len()).unwrap_or(u32::MAX) < maximum_conflicts {
520                conflicts.push(RebaseConflict {
521                    region: region.clone(),
522                    usage: captured.usage,
523                    expected: captured.expected,
524                    actual: actual.value,
525                });
526            } else {
527                truncated = true;
528            }
529        }
530    }
531    let decision = if conflicts.is_empty() {
532        RebaseDecision::Safe {
533            generation: candidate,
534        }
535    } else {
536        RebaseDecision::Conflicted {
537            conflicts,
538            truncated,
539        }
540    };
541    Ok(RebaseReceipt { decision, work })
542}
543
544/// Dependency-proof construction errors.
545#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
546pub enum DependencyError {
547    /// Dependency admission bound must be non-zero.
548    #[error("maximum dependencies must be non-zero")]
549    ZeroLimit,
550    /// Content ranges must contain at least one byte.
551    #[error("content dependency range is empty")]
552    EmptyContentRange,
553    /// Content range arithmetic overflowed.
554    #[error("content dependency range overflowed")]
555    RangeOverflow,
556    /// Directory listing observations require a positive page bound.
557    #[error("directory dependency page limit must be non-zero")]
558    ZeroDirectoryPageLimit,
559    /// The same exact region was captured with two different base states.
560    #[error("dependency region has contradictory captured states")]
561    ContradictoryState,
562    /// Distinct dependency count exceeded its hard bound.
563    #[error("dependency count exceeds maximum {maximum}")]
564    TooManyDependencies {
565        /// Configured maximum.
566        maximum: u32,
567    },
568}
569
570#[cfg(test)]
571#[path = "tests/rebase.rs"]
572mod tests;