1use 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#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
13pub enum DependencyRegion {
14 FileRecord(FileId),
16 Metadata(FileId),
18 FileLength(FileId),
20 ContentRange {
22 file_id: FileId,
24 offset: u64,
26 length: u64,
28 },
29 SparseSeek {
31 file_id: FileId,
33 offset: u64,
35 target: ExtentSeekTarget,
37 },
38 DirectoryName {
40 directory_id: FileId,
42 name: LogicalName,
44 },
45 DirectoryRange {
47 directory_id: FileId,
49 after: Option<LogicalName>,
51 maximum_entries: u32,
53 },
54}
55
56impl DependencyRegion {
57 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum DependencyState {
89 Absent,
91 Present(Digest),
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct Dependency {
98 pub region: DependencyRegion,
100 pub expected: DependencyState,
102}
103
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106pub enum DependencyUse {
107 Observation,
109 Mutation,
111 ObservationAndMutation,
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116struct CapturedState {
117 expected: DependencyState,
118 usage: DependencyUse,
119}
120
121#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct CheckoutDependencies {
127 captured: BTreeMap<DependencyRegion, CapturedState>,
128}
129
130impl CheckoutDependencies {
131 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 #[must_use]
182 pub fn len(&self) -> usize {
183 self.captured.len()
184 }
185
186 #[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(®ion) {
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
284pub trait RebaseProbe {
289 type Error: std::error::Error;
291
292 fn probe(
299 &self,
300 generation: GenerationId,
301 region: &DependencyRegion,
302 budget: WorkBudget,
303 ) -> Result<ProbeReceipt, OperationFailure<Self::Error>>;
304}
305
306pub trait AsyncRebaseProbe {
308 type Error: std::error::Error;
310
311 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#[derive(Clone, Debug, Eq, PartialEq)]
323pub struct ProbeReceipt {
324 pub value: DependencyState,
326 pub work: WorkCounters,
328}
329
330#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct RebaseConflict {
333 pub region: DependencyRegion,
335 pub usage: DependencyUse,
337 pub expected: DependencyState,
339 pub actual: DependencyState,
341}
342
343#[derive(Clone, Debug, Eq, PartialEq)]
345pub enum RebaseDecision {
346 Safe {
348 generation: GenerationId,
350 },
351 Conflicted {
353 conflicts: Vec<RebaseConflict>,
355 truncated: bool,
357 },
358}
359
360#[derive(Clone, Debug, Eq, PartialEq)]
362pub struct RebaseReceipt {
363 pub decision: RebaseDecision,
365 pub work: WorkCounters,
367}
368
369#[derive(Debug, Error)]
371pub enum RebaseError<E: std::error::Error> {
372 #[error("safe rebase classification was cancelled")]
374 Cancelled,
375 #[error("maximum rebase conflicts must be non-zero")]
377 ZeroConflictLimit,
378 #[error("rebase region probe failed: {0}")]
380 Probe(E),
381 #[error(transparent)]
383 Work(#[from] WorkError),
384}
385
386pub 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
464pub 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#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
546pub enum DependencyError {
547 #[error("maximum dependencies must be non-zero")]
549 ZeroLimit,
550 #[error("content dependency range is empty")]
552 EmptyContentRange,
553 #[error("content dependency range overflowed")]
555 RangeOverflow,
556 #[error("directory dependency page limit must be non-zero")]
558 ZeroDirectoryPageLimit,
559 #[error("dependency region has contradictory captured states")]
561 ContradictoryState,
562 #[error("dependency count exceeds maximum {maximum}")]
564 TooManyDependencies {
565 maximum: u32,
567 },
568}
569
570#[cfg(test)]
571#[path = "tests/rebase.rs"]
572mod tests;