1use super::{FileRecord, MetadataField, NamespacePath};
4use crate::foundation::FileId;
5use crate::model::VolumeLimits;
6use crate::performance::{OperationFailure, WorkBudget, WorkCounters, WorkError};
7use crate::storage::{ObjectId, ObjectKind};
8use std::cell::Cell;
9use std::mem::size_of;
10use thiserror::Error;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum Mutation {
15 Create {
17 path: NamespacePath,
19 record: FileRecord,
21 },
22 Restore {
26 path: NamespacePath,
28 record: FileRecord,
31 },
32 Remove {
34 path: NamespacePath,
36 expected_file_id: MetadataField<FileId>,
38 },
39 Rename {
41 source: NamespacePath,
43 destination: NamespacePath,
45 replace: bool,
47 },
48 Link {
50 source: NamespacePath,
52 destination: NamespacePath,
54 },
55 SetMetadata {
57 path: NamespacePath,
59 metadata: ObjectId,
61 },
62 Write {
64 path: NamespacePath,
66 offset: u64,
68 length: u64,
70 content: ObjectId,
72 content_offset: u64,
74 },
75 ValidateRegular {
77 path: NamespacePath,
79 },
80 Resize {
82 path: NamespacePath,
84 logical_bytes: u64,
86 },
87 ZeroRange {
89 path: NamespacePath,
91 offset: u64,
93 length: u64,
95 allocated: bool,
97 extend: bool,
99 },
100 Preallocate {
102 path: NamespacePath,
104 offset: u64,
106 length: u64,
108 keep_size: bool,
110 },
111 CloneRange {
113 source: NamespacePath,
115 source_offset: u64,
117 destination: NamespacePath,
119 destination_offset: u64,
121 length: u64,
123 },
124 File {
126 file_id: FileId,
128 mutation: FileMutation,
130 },
131 CloneFileRange {
133 source_file_id: FileId,
135 source_offset: u64,
137 destination_file_id: FileId,
139 destination_offset: u64,
141 length: u64,
143 },
144}
145
146#[derive(Clone, Debug, Eq, PartialEq)]
148pub enum FileMutation {
149 ReplaceRecord {
152 record: FileRecord,
154 },
155 SetMetadata {
157 metadata: ObjectId,
159 },
160 Write {
162 offset: u64,
164 length: u64,
166 content: ObjectId,
168 content_offset: u64,
170 },
171 ValidateRegular,
173 Resize {
175 logical_bytes: u64,
177 },
178 ZeroRange {
180 offset: u64,
182 length: u64,
184 allocated: bool,
186 extend: bool,
188 },
189 Preallocate {
191 offset: u64,
193 length: u64,
195 keep_size: bool,
197 },
198}
199
200impl Mutation {
201 pub(crate) fn paths(&self) -> Option<[&NamespacePath; 2]> {
202 let paths = match self {
203 Self::Create { path, .. }
204 | Self::Restore { path, .. }
205 | Self::Remove { path, .. }
206 | Self::SetMetadata { path, .. }
207 | Self::Write { path, .. }
208 | Self::ValidateRegular { path }
209 | Self::Resize { path, .. }
210 | Self::ZeroRange { path, .. }
211 | Self::Preallocate { path, .. } => [path, path],
212 Self::Rename {
213 source,
214 destination,
215 ..
216 }
217 | Self::Link {
218 source,
219 destination,
220 }
221 | Self::CloneRange {
222 source,
223 destination,
224 ..
225 } => [source, destination],
226 Self::File { .. } | Self::CloneFileRange { .. } => return None,
227 };
228 Some(paths)
229 }
230
231 fn validate(&self) -> Result<(), MutationPlanError> {
232 if let Some([first, second]) = self.paths()
233 && (first.is_root() || second.is_root())
234 {
235 return Err(MutationPlanError::RootMutation);
236 }
237 match self {
238 Self::Create { record, .. } | Self::Restore { record, .. }
239 if record.validate().is_err() =>
240 {
241 Err(MutationPlanError::InvalidInitialRecord)
242 }
243 Self::File {
244 file_id,
245 mutation: FileMutation::ReplaceRecord { record },
246 } if record.validate().is_err() || record.file_id != *file_id => {
247 Err(MutationPlanError::InvalidInitialRecord)
248 }
249 Self::Rename {
250 source,
251 destination,
252 ..
253 }
254 | Self::Link {
255 source,
256 destination,
257 } if source == destination => Err(MutationPlanError::SameSourceAndDestination),
258 Self::Write {
259 offset,
260 length,
261 content,
262 content_offset,
263 ..
264 }
265 | Self::File {
266 mutation:
267 FileMutation::Write {
268 offset,
269 length,
270 content,
271 content_offset,
272 },
273 ..
274 } => {
275 validate_range(*offset, *length)?;
276 validate_range(*content_offset, *length)?;
277 if content.kind != ObjectKind::Blob {
278 return Err(MutationPlanError::WrongContentKind);
279 }
280 Ok(())
281 }
282 Self::ZeroRange { offset, length, .. } | Self::Preallocate { offset, length, .. } => {
283 validate_range(*offset, *length)
284 }
285 Self::CloneRange {
286 source_offset,
287 destination_offset,
288 length,
289 ..
290 }
291 | Self::CloneFileRange {
292 source_offset,
293 destination_offset,
294 length,
295 ..
296 } => {
297 validate_range(*source_offset, *length)?;
298 validate_range(*destination_offset, *length)
299 }
300 Self::SetMetadata { metadata, .. } if metadata.kind != ObjectKind::Metadata => {
301 Err(MutationPlanError::WrongMetadataKind)
302 }
303 Self::File {
304 mutation: FileMutation::SetMetadata { metadata },
305 ..
306 } if metadata.kind != ObjectKind::Metadata => Err(MutationPlanError::WrongMetadataKind),
307 Self::File {
308 mutation:
309 FileMutation::ZeroRange { offset, length, .. }
310 | FileMutation::Preallocate { offset, length, .. },
311 ..
312 } => validate_range(*offset, *length),
313 Self::Create { .. }
314 | Self::Restore { .. }
315 | Self::Remove { .. }
316 | Self::Rename { .. }
317 | Self::Link { .. }
318 | Self::SetMetadata { .. }
319 | Self::ValidateRegular { .. }
320 | Self::Resize { .. }
321 | Self::File {
322 mutation:
323 FileMutation::SetMetadata { .. }
324 | FileMutation::ReplaceRecord { .. }
325 | FileMutation::ValidateRegular
326 | FileMutation::Resize { .. },
327 ..
328 } => Ok(()),
329 }
330 }
331}
332
333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
334struct PathUse {
335 operation: u32,
336 endpoint: u8,
337}
338
339#[derive(Clone, Debug, Eq, PartialEq)]
341pub struct MutationPlan {
342 operations: Vec<Mutation>,
343 ordered_paths: Vec<PathUse>,
344 path_nodes: u64,
345 retained_allocation_bytes: u64,
346 work: WorkCounters,
347}
348
349impl MutationPlan {
350 pub fn compile(
357 operations: Vec<Mutation>,
358 limits: VolumeLimits,
359 budget: WorkBudget,
360 ) -> Result<Self, MutationPlanFailure> {
361 if operations.is_empty() {
362 return Err(OperationFailure::before_work(MutationPlanError::Empty));
363 }
364 if u32::try_from(operations.len()).unwrap_or(u32::MAX) > limits.maximum_mutations_per_batch
365 {
366 return Err(OperationFailure::before_work(
367 MutationPlanError::TooManyMutations,
368 ));
369 }
370 let mut work = WorkCounters::default();
371 for operation in &operations {
372 operation
373 .validate()
374 .map_err(|error| OperationFailure::new(error, work))?;
375 }
376
377 let maximum_uses = path_use_count(&operations)
378 .ok_or_else(|| OperationFailure::new(MutationPlanError::IndexOverflow, work))?;
379 let allocation_bytes = maximum_uses
380 .checked_mul(size_of::<PathUse>())
381 .map(crate::foundation::usize_to_u64)
382 .ok_or_else(|| OperationFailure::new(MutationPlanError::IndexOverflow, work))?;
383 let mut ordered_paths = Vec::new();
384 if maximum_uses != 0 {
385 let attempted = work
386 .checked_add(WorkCounters {
387 allocation_operations: 1,
388 ..WorkCounters::default()
389 })
390 .map_err(|error| OperationFailure::new(error.into(), work))?;
391 attempted
392 .verify(budget)
393 .map_err(|error| OperationFailure::new(error.into(), work))?;
394 let mut allocated = attempted;
395 allocated.peak_allocation_bytes = allocated.peak_allocation_bytes.max(allocation_bytes);
396 allocated
397 .verify(budget)
398 .map_err(|error| OperationFailure::new(error.into(), work))?;
399 ordered_paths.try_reserve_exact(maximum_uses).map_err(|_| {
400 OperationFailure::new(MutationPlanError::AllocationFailed, attempted)
401 })?;
402 work = allocated;
403 }
404 for (index, operation) in operations.iter().enumerate() {
405 let operation_index = u32::try_from(index)
406 .map_err(|_| OperationFailure::new(MutationPlanError::IndexOverflow, work))?;
407 let Some(paths) = operation.paths() else {
408 continue;
409 };
410 ordered_paths.push(PathUse {
411 operation: operation_index,
412 endpoint: 0,
413 });
414 if paths[1] != paths[0] {
415 ordered_paths.push(PathUse {
416 operation: operation_index,
417 endpoint: 1,
418 });
419 }
420 }
421 let comparisons = Cell::new(0_u64);
422 ordered_paths.sort_unstable_by(|left, right| {
423 comparisons.set(comparisons.get().saturating_add(1));
424 path_for(&operations, *left)
425 .cmp(path_for(&operations, *right))
426 .then_with(|| left.operation.cmp(&right.operation))
427 .then_with(|| left.endpoint.cmp(&right.endpoint))
428 });
429 let mut path_nodes = 1_u64;
430 let mut prior: Option<&NamespacePath> = None;
431 let mut component_visits = 0_u64;
432 for path_use in &ordered_paths {
433 let path = path_for(&operations, *path_use);
434 component_visits = component_visits
435 .checked_add(u64::try_from(path.depth()).unwrap_or(u64::MAX))
436 .ok_or_else(|| OperationFailure::new(MutationPlanError::IndexOverflow, work))?;
437 let shared = prior.map_or(0, |previous| shared_prefix(previous, path));
438 path_nodes = path_nodes
439 .checked_add(u64::try_from(path.depth() - shared).unwrap_or(u64::MAX))
440 .ok_or_else(|| OperationFailure::new(MutationPlanError::IndexOverflow, work))?;
441 prior = Some(path);
442 }
443 work = work
444 .checked_add(WorkCounters {
445 items_examined: component_visits
446 .checked_add(comparisons.get())
447 .ok_or_else(|| OperationFailure::new(MutationPlanError::IndexOverflow, work))?,
448 ..WorkCounters::default()
449 })
450 .map_err(|error| OperationFailure::new(error.into(), work))?;
451 work.verify(budget)
452 .map_err(|error| OperationFailure::new(error.into(), work))?;
453 Ok(Self {
454 operations,
455 ordered_paths,
456 path_nodes,
457 retained_allocation_bytes: allocation_bytes,
458 work,
459 })
460 }
461
462 #[must_use]
464 pub fn operations(&self) -> &[Mutation] {
465 &self.operations
466 }
467
468 #[must_use]
473 pub fn ordered_paths(&self) -> impl ExactSizeIterator<Item = &NamespacePath> {
474 self.ordered_paths
475 .iter()
476 .map(|path_use| path_for(&self.operations, *path_use))
477 }
478
479 #[must_use]
485 pub fn ordered_operation_paths(
486 &self,
487 ) -> impl ExactSizeIterator<Item = (usize, usize, &NamespacePath)> {
488 self.ordered_paths.iter().map(|path_use| {
489 (
490 usize::try_from(path_use.operation).unwrap_or(usize::MAX),
491 usize::from(path_use.endpoint),
492 path_for(&self.operations, *path_use),
493 )
494 })
495 }
496
497 #[must_use]
499 pub const fn path_nodes(&self) -> u64 {
500 self.path_nodes
501 }
502
503 #[must_use]
505 pub const fn retained_allocation_bytes(&self) -> u64 {
506 self.retained_allocation_bytes
507 }
508
509 #[must_use]
511 pub const fn work(&self) -> WorkCounters {
512 self.work
513 }
514
515 pub(crate) fn into_operations(self) -> Vec<Mutation> {
516 self.operations
517 }
518}
519
520#[allow(clippy::expect_used)]
523#[allow(
524 clippy::indexing_slicing,
525 reason = "PathUse is private and is only ever constructed in MutationPlan::compile as `operation: u32::try_from(index)` for `index` in `operations.iter().enumerate()`; operations and ordered_paths are stored together in MutationPlan and neither is mutated afterward, so path_use.operation as usize is always < operations.len()"
526)]
527fn path_for(operations: &[Mutation], path_use: PathUse) -> &NamespacePath {
528 let operation = &operations[usize::try_from(path_use.operation).unwrap_or(usize::MAX)];
529 let [first, second] = operation
530 .paths()
531 .expect("path use must reference a path mutation");
532 match path_use.endpoint {
535 0 => first,
536 _ => second,
537 }
538}
539
540fn shared_prefix(left: &NamespacePath, right: &NamespacePath) -> usize {
541 left.components()
542 .iter()
543 .zip(right.components())
544 .take_while(|(left, right)| left == right)
545 .count()
546}
547
548fn path_use_count(operations: &[Mutation]) -> Option<usize> {
549 operations.iter().try_fold(0_usize, |count, operation| {
550 let uses = operation
551 .paths()
552 .map_or(0, |[first, second]| if first == second { 1 } else { 2 });
553 count.checked_add(uses)
554 })
555}
556
557fn validate_range(offset: u64, length: u64) -> Result<(), MutationPlanError> {
558 if length == 0 {
559 return Err(MutationPlanError::EmptyRange);
560 }
561 offset
562 .checked_add(length)
563 .ok_or(MutationPlanError::RangeOverflow)?;
564 Ok(())
565}
566
567pub type MutationPlanFailure = OperationFailure<MutationPlanError>;
569
570#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
572pub enum MutationPlanError {
573 #[error("mutation batch is empty")]
575 Empty,
576 #[error("mutation batch exceeds the volume limit")]
578 TooManyMutations,
579 #[error("created file record is invalid")]
581 InvalidInitialRecord,
582 #[error("mutation cannot target the volume root")]
584 RootMutation,
585 #[error("mutation source and destination are identical")]
587 SameSourceAndDestination,
588 #[error("mutation byte range is empty")]
590 EmptyRange,
591 #[error("mutation byte range overflowed")]
593 RangeOverflow,
594 #[error("mutation content object is not a blob")]
596 WrongContentKind,
597 #[error("mutation metadata object has the wrong kind")]
599 WrongMetadataKind,
600 #[error("mutation-plan index overflowed")]
602 IndexOverflow,
603 #[error("mutation-plan allocation failed")]
605 AllocationFailed,
606 #[error(transparent)]
608 Work(#[from] WorkError),
609}
610
611#[cfg(test)]
612#[path = "tests/mutation.rs"]
613mod tests;