use crate::async_storage::{AsyncAuthorityStore, AsyncObjectStore};
use crate::cancellation::{CancellationError, CancellationToken};
use crate::foundation::{
Digest, DurableCommit, Epoch, FileId, GenerationId, Head, OperationId, ProposedCommit,
Sequence, VolumeId,
};
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
use crate::kernel::decode_retention_created;
use crate::kernel::{
AsyncBlobSource, AttributeEntry, AttributeListError, AttributeListing, AttributeLookupError,
AttributeMutation, AttributeMutationError, AttributeName, AttributePage,
AuthenticatedGenerationProbe, AuthenticatedProbeError, BlobBuildError, BlobBuildOptions,
BlobReadError, CanonicalDecodeError, CheckoutDependencies, CheckpointError, CheckpointRequest,
ClosureError, ClosureLimits, DecodeLimits, Dependency, DependencyError, DependencyRegion,
DependencyState, DirectoryPage, DirectoryReadError, ExtentPlan, ExtentRangeRequest,
ExtentSeekRequest, ExtentSeekTarget, FileKind, FileMetadata, FileMutation, FilePayload,
FileRangeRead, FileRangeReadError, FileRangeRequest, FileRecord, FileRecordReadError,
FileTableMutationError, FileTablePage, GenerationExportManifest, GenerationMutationError,
GenerationRoot, GenerationTransferBatch, GenerationTransferError, InlineFileData,
InlineFileDataError, LivePublicationObservation, LiveRetryAction, LiveRetryError,
LiveRetryState, LogicalName, MAXIMUM_GENERATION_ROOT_BYTES, MergeConflictResolution,
MergeConflictSide, MergeGenerationError, MergeGenerationOutcome, MergeGenerationRequest,
MetadataField, Mutation, NameEncoding, NamespacePath, PathBatchLookup, PathLookup,
PathLookupError, PersistentDiffError, ProbeLimits, PublicationError, PublicationIntent,
PublicationReceipt, PublishGenerationRequest, RebaseConflict, RebaseDecision, RebaseError,
RegularMutation, RegularMutationError, RetentionCreated, RetentionCreatedError, RetentionKind,
TransferCursor, TreeMutationError, TreePage, VolumeCreated, VolumeCreatedError,
apply_attribute_mutations_async, apply_generation_mutations_retaining_async,
apply_regular_mutation_async, authenticate_generation_export_manifest_async, build_blob_async,
build_checkpoint_async, build_generation_export_manifest_async, classify_rebase_async,
decode_file_metadata, decode_published_generation, decode_volume_created,
decode_workspace_deleted, diff_file_records_async, diff_tree_entries_async,
encode_attribute_page, encode_file_metadata, encode_file_table_page, encode_generation_root,
encode_retention_created, encode_tree_page, encode_volume_created, encode_workspace_deleted,
export_generation_batch_async, generation_root_parent_count, import_generation_batch_async,
list_attributes_async, list_tree_entries_async, list_tree_entries_at_or_after_async,
lookup_attribute_async, lookup_file_record_async, lookup_file_records_async,
merge_generation_async, plan_extent_range_async, prove_generation_closure_async,
publish_generation_async, publish_generation_async_with_context,
publish_generation_async_with_permit, read_blob_range_async, read_file_range_async,
retention_authority_id, seek_extent_async, volume_authority_id,
};
#[cfg(test)]
use crate::kernel::{
FileTableMutation, TreeMutation, file_table_mutation,
merge_directory_record_async as merge_directory_record, merge_file_fields, resolve_three,
tree_mutation,
};
pub use crate::kernel::{LiveMutationOutcome, MergeConflict};
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
use crate::kernel::{decode_source_volume, source_authority_id};
use crate::model::{
AccessMode, CaseSensitivity, CheckoutMode, CheckoutModeError, ConcurrencyMode, ConsistencyMode,
GenerationSelector, Lifecycle, MutationMode, UnicodePolicy, VolumeConfig, VolumeConfigError,
VolumeLimits,
};
use crate::performance::{
MeasuredResult, OperationFailure, OperationReceipt, WorkBudget, WorkCounters, WorkError,
};
use crate::storage::{
AppendOutcome, AuthorityStoreError, ByteRange, CreateAuthorityOutcome, FenceOutcome,
OBJECT_DIGEST_ENVELOPE_BYTES, ObjectId, ObjectKind, ObjectReadRequest, ObjectReadRetention,
ObjectStoreError, PublicationPermit, ReplayLimit, object_digest,
};
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
use acyclic_native_runtime::OwnershipAnchor;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
use acyclic_objects::ObjectsProvider as _;
use bytes::Bytes;
use futures::{StreamExt as _, stream};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::mem::size_of;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
use std::sync::{OnceLock, Weak};
use thiserror::Error;
const MAXIMUM_VOLUME_EVENT_BYTES: u64 = 4 * 1024;
const STAGED_METADATA_CACHE_ENTRIES: usize = 16;
struct AuthoredMetadataCache {
entries: [Option<(FileMetadata, ObjectId)>; STAGED_METADATA_CACHE_ENTRIES],
next: usize,
}
impl AuthoredMetadataCache {
fn new() -> Self {
Self {
entries: [None; STAGED_METADATA_CACHE_ENTRIES],
next: 0,
}
}
fn get(&self, metadata: FileMetadata) -> Option<ObjectId> {
self.entries
.iter()
.flatten()
.find_map(|(cached, object)| (*cached == metadata).then_some(*object))
}
fn remember(&mut self, metadata: FileMetadata, object: ObjectId) {
if let Some(slot) = self.entries.get_mut(self.next) {
*slot = Some((metadata, object));
}
self.next = (self.next + 1) % STAGED_METADATA_CACHE_ENTRIES;
}
}
const CASE_FOLD_COLLISION_SCAN_PAGE: u32 = 256;
const ROOT_FILE_DOMAIN: &[u8] = b"acyclic-fs-root-file-v1\0";
const CREATION_OPERATION_DOMAIN: &[u8] = b"acyclic-fs-create-operation-v1\0";
const CREATION_FINGERPRINT_DOMAIN: &[u8] = b"acyclic-fs-create-fingerprint-v1\0";
const WORKSPACE_JOIN_OPERATION_DOMAIN: &[u8] = b"acyclic-fs-workspace-join-operation-v1\0";
pub(crate) enum WorkspaceJoinOutcome {
Applied(GenerationId),
AlreadyApplied(GenerationId),
NoChanges(GenerationId),
Stale(GenerationId),
Conflicted(Vec<MergeConflict>, bool),
Fenced,
IdempotencyConflict,
}
pub(crate) struct WorkspaceJoinRequest<'a, A, O> {
pub target: &'a Volume<A, O>,
pub base: WorkspaceCommonAncestor,
pub source: &'a crate::Generation<A, O>,
pub expected_target: GenerationId,
pub expected_head: Head,
pub history: crate::workspace::JoinHistory,
pub operation_id: OperationId,
pub permit: PublicationPermit,
pub maximum_generations: u32,
pub maximum_changes: u32,
pub maximum_conflicts: u32,
pub resolutions: BTreeMap<MergeConflict, MergeConflictResolution>,
}
impl<A, O> WorkspaceJoinRequest<'_, A, O> {
fn operation_context(&self) -> Digest {
let mut hasher = blake3::Hasher::new();
hasher.update(WORKSPACE_JOIN_OPERATION_DOMAIN);
hasher.update(&self.source.workspace.volume.id.into_bytes());
hasher.update(self.source.id.digest().as_bytes());
hasher.update(&self.target.id.into_bytes());
hasher.update(self.expected_target.digest().as_bytes());
hasher.update(&self.base.volume_id.into_bytes());
hasher.update(self.base.id.digest().as_bytes());
hasher.update(&[match self.history {
crate::workspace::JoinHistory::Merge => 1,
crate::workspace::JoinHistory::Rebase => 2,
crate::workspace::JoinHistory::Squash => 3,
crate::workspace::JoinHistory::CherryPick => 4,
}]);
hasher.update(&self.maximum_generations.to_le_bytes());
hasher.update(&self.maximum_changes.to_le_bytes());
hasher.update(&self.maximum_conflicts.to_le_bytes());
for (conflict, resolution) in &self.resolutions {
match conflict {
MergeConflict::File(file_id) => {
hasher.update(&[0]);
hasher.update(&file_id.into_bytes());
}
MergeConflict::Binding { directory_id, name } => {
hasher.update(&[1]);
hasher.update(&directory_id.into_bytes());
hasher.update(&[match name.encoding() {
NameEncoding::Utf8 => 0,
NameEncoding::PosixBytes => 1,
NameEncoding::WindowsUtf16Le => 2,
}]);
hasher.update(&(name.as_bytes().len() as u64).to_le_bytes());
hasher.update(name.as_bytes());
}
}
hash_merge_resolution(&mut hasher, resolution);
}
Digest::from_bytes(*hasher.finalize().as_bytes())
}
}
fn hash_merge_resolution(hasher: &mut blake3::Hasher, resolution: &MergeConflictResolution) {
let hash_side = |hasher: &mut blake3::Hasher, side: MergeConflictSide| {
hasher.update(&[match side {
MergeConflictSide::Base => 0,
MergeConflictSide::Ours => 1,
MergeConflictSide::Theirs => 2,
}]);
};
match resolution {
MergeConflictResolution::Select(side) => {
hasher.update(&[0]);
hash_side(hasher, *side);
}
MergeConflictResolution::File(record) => {
hasher.update(&[1]);
if let Some(record) = record {
hasher.update(&[1]);
hasher.update(&record.file_id.into_bytes());
hasher.update(&[match record.kind {
FileKind::Regular => 0,
FileKind::Directory => 1,
FileKind::SymbolicLink => 2,
FileKind::Fifo => 3,
FileKind::Socket => 4,
FileKind::CharacterDevice => 5,
FileKind::BlockDevice => 6,
FileKind::ReparsePoint => 7,
FileKind::MountBoundary => 8,
}]);
hasher.update(&record.link_count.to_le_bytes());
hash_object_id(hasher, record.metadata);
hash_file_payload(hasher, record.payload);
} else {
hasher.update(&[0]);
}
}
MergeConflictResolution::Binding(file_id) => {
hasher.update(&[2]);
if let Some(file_id) = file_id {
hasher.update(&[1]);
hasher.update(&file_id.into_bytes());
} else {
hasher.update(&[0]);
}
}
}
}
fn hash_object_id(hasher: &mut blake3::Hasher, object: ObjectId) {
hasher.update(&[object.kind.canonical_tag()]);
hasher.update(object.digest.as_bytes());
}
fn hash_file_payload(hasher: &mut blake3::Hasher, payload: FilePayload) {
match payload {
FilePayload::InlineRegular(data) => {
hasher.update(&[0]);
hasher.update(&(data.as_bytes().len() as u64).to_le_bytes());
hasher.update(data.as_bytes());
}
FilePayload::Regular {
logical_bytes,
extents,
} => {
hasher.update(&[1]);
hasher.update(&logical_bytes.to_le_bytes());
hash_object_id(hasher, extents);
}
FilePayload::Directory { entries } => {
hasher.update(&[2]);
hash_object_id(hasher, entries);
}
FilePayload::SymbolicLink {
target_bytes,
target,
} => {
hasher.update(&[3]);
hasher.update(&target_bytes.to_le_bytes());
hash_object_id(hasher, target);
}
FilePayload::Empty => {
hasher.update(&[4]);
}
FilePayload::Device { major, minor } => {
hasher.update(&[5]);
hasher.update(&major.to_le_bytes());
hasher.update(&minor.to_le_bytes());
}
FilePayload::ReparsePoint {
payload_bytes,
payload,
} => {
hasher.update(&[6]);
hasher.update(&payload_bytes.to_le_bytes());
hash_object_id(hasher, payload);
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct WorkspaceCommonAncestor {
pub id: GenerationId,
pub volume_id: VolumeId,
}
pub(crate) struct WorkspaceRebaseRequest<'a, A, O> {
pub target: &'a Volume<A, O>,
pub operation_id: OperationId,
pub maximum_generations: u32,
pub maximum_changes: u32,
pub maximum_conflicts: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EmbeddedCapabilities {
pub durable: bool,
}
impl EmbeddedCapabilities {
pub const MEMORY: Self = Self { durable: false };
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LocalOptions {
pub root: PathBuf,
pub stream: acyclic_stream::LocalStreamLimits,
pub objects: acyclic_objects::LocalObjectsLimits,
pub object_cache: crate::cache::ObjectCacheOptions,
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct LocalGarbageCollection {
pub examined: u64,
pub removed: u64,
pub segments_removed: u64,
pub temporary_files_removed: u64,
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
impl LocalOptions {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
stream: acyclic_stream::LocalStreamLimits::default(),
objects: acyclic_objects::LocalObjectsLimits::default(),
object_cache: crate::cache::ObjectCacheOptions::default(),
}
}
}
struct FsInner<A, O> {
authority: A,
objects: O,
capabilities: EmbeddedCapabilities,
workspace_namespace: [u8; 16],
path_index: Arc<dyn crate::path_index::GenerationPathIndex>,
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
_local_root_lifecycle: Option<OwnershipAnchor>,
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
struct LocalRootRegistration {
options: LocalOptions,
live: Weak<FsInner<LocalAuthorityBackend, LocalObjectBackend>>,
opening: Arc<tokio::sync::Mutex<()>>,
lifecycle: Arc<tokio::sync::Mutex<()>>,
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
type LocalRootRegistry = std::collections::BTreeMap<PathBuf, LocalRootRegistration>;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
fn prepare_local_root_open(
registry: &mut LocalRootRegistry,
root: &Path,
options: &LocalOptions,
) -> Arc<tokio::sync::Mutex<()>> {
if let Some(registration) = registry.get(root) {
return Arc::clone(®istration.opening);
}
let opening = Arc::new(tokio::sync::Mutex::new(()));
let lifecycle = Arc::new(tokio::sync::Mutex::new(()));
registry.insert(
root.to_path_buf(),
LocalRootRegistration {
options: options.clone(),
live: Weak::new(),
opening: Arc::clone(&opening),
lifecycle: Arc::clone(&lifecycle),
},
);
opening
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
async fn run_local_initialization<T, F, I>(
ownership: tokio::sync::OwnedMutexGuard<()>,
initialize: I,
) -> Result<T, FsError>
where
T: Send + 'static,
F: std::future::Future<Output = T> + Send + 'static,
I: FnOnce(tokio::sync::OwnedMutexGuard<()>) -> F + Send + 'static,
{
tokio::spawn(async move { initialize(ownership).await })
.await
.map_err(|_| FsError::LocalInitializationWorker)
}
pub struct Fs<A, O> {
inner: Arc<FsInner<A, O>>,
}
impl<A, O> Clone for Fs<A, O> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<A, O> Fs<A, O> {
#[must_use]
pub fn capabilities(&self) -> EmbeddedCapabilities {
self.inner.capabilities
}
pub fn workspace_id(
&self,
name: impl AsRef<str>,
) -> Result<crate::WorkspaceId, crate::WorkspaceNameError> {
let name = crate::WorkspaceName::new(name)?;
Ok(crate::WorkspaceId::derive(
self.inner.workspace_namespace,
&name,
))
}
pub(crate) fn same_deployment(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
pub(crate) fn path_index(&self) -> &dyn crate::path_index::GenerationPathIndex {
&*self.inner.path_index
}
pub(crate) fn authority(&self) -> &A {
&self.inner.authority
}
#[cfg(all(test, feature = "s3-http"))]
pub(crate) fn objects(&self) -> &O {
&self.inner.objects
}
}
impl<P, O> Fs<crate::StreamAuthorityStore<P>, O> {
#[must_use]
pub fn operation_window_store(&self) -> crate::StreamOperationWindowStore<P> {
crate::StreamOperationWindowStore::new(self.inner.authority.provider())
}
}
pub struct Volume<A, O> {
pub(crate) fs: Fs<A, O>,
pub(crate) id: VolumeId,
pub(crate) config: VolumeConfig,
}
impl<A, O> Clone for Volume<A, O> {
fn clone(&self) -> Self {
Self {
fs: self.fs.clone(),
id: self.id,
config: self.config,
}
}
}
pub struct Checkout<A, O> {
volume: Volume<A, O>,
base_generation_root: ObjectId,
generation_root: ObjectId,
base_file_table: ObjectId,
base_root: GenerationRoot,
root: GenerationRoot,
authority_head: Option<Head>,
authored_operation_id: Option<OperationId>,
live_operation_id: Option<OperationId>,
last_commit: Option<LastCommit>,
prepared_merge_parent: Option<ObjectId>,
dependencies: CheckoutDependencies,
mode: CheckoutMode,
}
pub struct PinnedReader<A, O> {
volume: Volume<A, O>,
generation_root: ObjectId,
root: GenerationRoot,
}
pub struct ContentStager<A, O> {
volume: Volume<A, O>,
}
impl<A, O> Clone for ContentStager<A, O> {
fn clone(&self) -> Self {
Self {
volume: self.volume.clone(),
}
}
}
impl<A, O> Clone for PinnedReader<A, O> {
fn clone(&self) -> Self {
Self {
volume: self.volume.clone(),
generation_root: self.generation_root,
root: self.root.clone(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileDescription {
pub kind: FileKind,
pub logical_bytes: u64,
pub metadata: FileMetadata,
}
pub struct ResolvedFile<A, O> {
reader: PinnedReader<A, O>,
record: FileRecord,
description: FileDescription,
}
impl<A, O> ResolvedFile<A, O> {
#[must_use]
pub const fn file_id(&self) -> FileId {
self.record.file_id
}
#[must_use]
pub const fn description(&self) -> &FileDescription {
&self.description
}
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> ResolvedFile<A, O> {
pub async fn read_range(
&self,
range: ByteRange,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
self.reader
.read_record_range(self.record, range, budget, cancellation)
.await
}
pub async fn read_symbolic_link(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Bytes> {
self.reader
.read_symbolic_link_record(self.record, budget, cancellation)
.await
}
pub async fn plan_extents(
&self,
range: ByteRange,
maximum_spans: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<ExtentPlan>> {
self.reader
.plan_record_extents(self.record, range, maximum_spans, budget, cancellation)
.await
}
}
pub struct ResolvedFileRangeReadRequest<'a, A, O> {
pub file: &'a ResolvedFile<A, O>,
pub range: ByteRange,
}
pub struct ResolvedDirectoryEntry<A, O> {
pub name: LogicalName,
pub file: ResolvedFile<A, O>,
}
pub struct ResolvedDirectoryPage<A, O> {
pub entries: Vec<ResolvedDirectoryEntry<A, O>>,
pub has_more: bool,
}
#[derive(Clone, Copy)]
struct LastCommit {
operation_id: OperationId,
generation_id: GenerationId,
head: Head,
}
#[derive(Clone, Copy)]
struct VolumeCreation {
volume_id: VolumeId,
config: VolumeConfig,
generation_root: ObjectId,
operation_id: Option<OperationId>,
}
#[derive(Clone, Copy)]
struct VerifiedForkSource {
generation_root: ObjectId,
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
pub type LocalAuthorityBackend =
crate::distributed::StreamAuthorityStore<acyclic_stream::LocalStream>;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
pub type LocalObjectBackend = crate::staged_objects::StagedObjects<
crate::cache::CachedObjectStore<
crate::distributed::ProviderObjectStore<acyclic_objects::LocalObjects>,
>,
>;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
pub type LocalFs = Fs<LocalAuthorityBackend, LocalObjectBackend>;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
pub type LocalOperationWindowStore = crate::StreamOperationWindowStore<acyclic_stream::LocalStream>;
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
pub type LocalVolume = Volume<LocalAuthorityBackend, LocalObjectBackend>;
#[cfg(all(feature = "memory", feature = "distributed"))]
pub type MemoryFs = Fs<MemoryAuthorityBackend, MemoryObjectBackend>;
#[cfg(all(feature = "memory", feature = "distributed"))]
pub type MemoryAuthorityBackend =
crate::distributed::StreamAuthorityStore<acyclic_stream::MemoryStream>;
#[cfg(all(feature = "memory", feature = "distributed"))]
pub type MemoryObjectBackend =
crate::distributed::ProviderObjectStore<acyclic_objects::MemoryObjects>;
#[cfg(all(test, feature = "memory", feature = "distributed"))]
pub(crate) type MemoryCheckout = Checkout<MemoryAuthorityBackend, MemoryObjectBackend>;
pub struct DetachedFile<A, O> {
volume: Volume<A, O>,
record: FileRecord,
}
pub type FsReceipt<T> = OperationReceipt<T>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileCloneRequest {
pub source: NamespacePath,
pub source_offset: u64,
pub destination: NamespacePath,
pub destination_offset: u64,
pub length: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AuthoredMutation {
Reidentify {
path: NamespacePath,
file_id: FileId,
},
CreateFile {
path: NamespacePath,
bytes: Bytes,
metadata: FileMetadata,
},
CreateFileFromContent {
path: NamespacePath,
content: StagedContent,
metadata: FileMetadata,
file_id: Option<FileId>,
},
CreateDirectory {
path: NamespacePath,
metadata: FileMetadata,
},
CreateSymbolicLink {
path: NamespacePath,
target: Bytes,
metadata: FileMetadata,
},
CreateEmptySpecial {
path: NamespacePath,
kind: FileKind,
metadata: FileMetadata,
},
CreateDevice {
path: NamespacePath,
kind: FileKind,
major: u32,
minor: u32,
metadata: FileMetadata,
},
CreateReparsePoint {
path: NamespacePath,
payload: Bytes,
metadata: FileMetadata,
},
Remove {
path: NamespacePath,
expected_file_id: Option<FileId>,
},
Rename {
source: NamespacePath,
destination: NamespacePath,
replace: bool,
},
HardLink {
source: NamespacePath,
destination: NamespacePath,
},
Write {
path: NamespacePath,
offset: u64,
bytes: Bytes,
},
WriteFromContent {
path: NamespacePath,
offset: u64,
content: StagedContent,
},
SetMetadata {
path: NamespacePath,
metadata: FileMetadata,
},
Resize {
path: NamespacePath,
logical_bytes: u64,
},
ZeroRange {
path: NamespacePath,
range: ByteRange,
allocated: bool,
extend: bool,
},
Preallocate {
path: NamespacePath,
range: ByteRange,
keep_size: bool,
},
CloneRange(FileCloneRequest),
}
fn authored_mutation_operation_count(mutation: &AuthoredMutation) -> usize {
match mutation {
AuthoredMutation::CreateFile { bytes, .. } => {
usize::from(bytes.len() > crate::kernel::MAXIMUM_INLINE_FILE_BYTES) + 1
}
AuthoredMutation::CreateFileFromContent { content, .. } => {
usize::from(content.logical_bytes != 0) + 1
}
AuthoredMutation::Reidentify { .. }
| AuthoredMutation::Write { .. }
| AuthoredMutation::WriteFromContent { .. }
| AuthoredMutation::CreateDirectory { .. }
| AuthoredMutation::CreateSymbolicLink { .. }
| AuthoredMutation::CreateEmptySpecial { .. }
| AuthoredMutation::CreateDevice { .. }
| AuthoredMutation::CreateReparsePoint { .. }
| AuthoredMutation::Remove { .. }
| AuthoredMutation::Rename { .. }
| AuthoredMutation::HardLink { .. }
| AuthoredMutation::SetMetadata { .. }
| AuthoredMutation::Resize { .. }
| AuthoredMutation::ZeroRange { .. }
| AuthoredMutation::Preallocate { .. }
| AuthoredMutation::CloneRange(_) => 1,
}
}
fn pending_name_collision(
path: &NamespacePath,
pending: &[(NamespacePath, FileId)],
allowed_file_ids: &[FileId],
) -> bool {
let Some((parent, name)) = path.split_last() else {
return false;
};
let folded = name.case_fold_key();
pending.iter().any(|(pending_path, file_id)| {
let Some((pending_parent, pending_name)) = pending_path.split_last() else {
return false;
};
pending_parent == parent
&& pending_name != name
&& pending_name.case_fold_key() == folded
&& !allowed_file_ids.contains(file_id)
})
}
fn pending_binding_path(
path: &NamespacePath,
pending: &[(NamespacePath, FileId)],
case_sensitivity: CaseSensitivity,
) -> Option<(NamespacePath, FileId)> {
let (parent, name) = path.split_last()?;
pending.iter().rev().find_map(|(pending_path, file_id)| {
let (pending_parent, pending_name) = pending_path.split_last()?;
let same_name = match case_sensitivity {
CaseSensitivity::Sensitive => pending_name == name,
CaseSensitivity::ProfileFolded => pending_name.case_fold_key() == name.case_fold_key(),
};
(pending_parent == parent && same_name).then_some((pending_path.clone(), *file_id))
})
}
fn pending_binding_id(
path: &NamespacePath,
pending: &[(NamespacePath, FileId)],
case_sensitivity: CaseSensitivity,
) -> Option<FileId> {
pending_binding_path(path, pending, case_sensitivity).map(|(_, file_id)| file_id)
}
fn pending_parent_exists(
path: &NamespacePath,
pending: &[(NamespacePath, FileId)],
case_sensitivity: CaseSensitivity,
limits: VolumeLimits,
) -> bool {
let Some((parent, _)) = path.split_last() else {
return false;
};
let parent_path = NamespacePath::new(parent.to_vec(), limits);
parent_path
.ok()
.and_then(|value| pending_binding_id(&value, pending, case_sensitivity))
.is_some()
}
fn removed_binding_id(path: &NamespacePath, removed: &[(NamespacePath, FileId)]) -> Option<FileId> {
let (parent, name) = path.split_last()?;
let folded = name.case_fold_key();
removed.iter().rev().find_map(|(removed_path, file_id)| {
let (removed_parent, removed_name) = removed_path.split_last()?;
(removed_parent == parent && removed_name.case_fold_key() == folded).then_some(*file_id)
})
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuthoredTransactionResult {
pub created_file_ids: Vec<Option<FileId>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuthoredLiveMutationResult {
pub created_file_ids: Vec<Option<FileId>>,
pub outcome: LiveMutationOutcome,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StagedContent {
root: ObjectId,
logical_bytes: u64,
}
impl StagedContent {
#[cfg(feature = "s3-http")]
pub(crate) const fn from_canonical_parts(root: ObjectId, logical_bytes: u64) -> Self {
Self {
root,
logical_bytes,
}
}
#[must_use]
pub const fn root(&self) -> ObjectId {
self.root
}
#[must_use]
pub const fn logical_bytes(&self) -> u64 {
self.logical_bytes
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DirectoryRecordEntry {
pub name: LogicalName,
pub record: FileRecord,
pub metadata: FileMetadata,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DirectoryRecordPage {
pub entries: Vec<DirectoryRecordEntry>,
pub has_more: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DirectoryPageRequest {
pub path: NamespacePath,
pub after: Option<LogicalName>,
pub maximum_entries: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PathMetadataLookup {
pub record: FileRecord,
pub metadata: FileMetadata,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NamedAttributeWriteMode {
Upsert,
Create,
Replace,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CheckoutCommitOutcome {
Committed {
generation_id: GenerationId,
head: Head,
},
AlreadyCommitted {
generation_id: GenerationId,
head: Head,
},
Conflict {
actual: Head,
},
Fenced {
actual_epoch: Epoch,
},
IdempotencyConflict {
committed_fingerprint: Digest,
},
}
pub type FsResult<T> = MeasuredResult<FsReceipt<T>, FsError>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FileRecordChange {
pub file_id: FileId,
pub before: Option<FileRecord>,
pub after: Option<FileRecord>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DirectoryBindingChange {
pub directory_id: FileId,
pub name: LogicalName,
pub before: Option<crate::kernel::TreeEntry>,
pub after: Option<crate::kernel::TreeEntry>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GenerationDiff {
pub files: Vec<FileRecordChange>,
pub bindings: Vec<DirectoryBindingChange>,
pub truncated: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MergePreparation {
Prepared {
generation_id: GenerationId,
},
Conflicted {
conflicts: Vec<MergeConflict>,
truncated: bool,
},
}
#[derive(Debug, Error)]
pub enum FsError {
#[error(transparent)]
Cancelled(#[from] CancellationError),
#[error(transparent)]
Config(#[from] VolumeConfigError),
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[error(transparent)]
LocalStream(#[from] acyclic_stream::LocalStreamError),
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[error(transparent)]
LocalObjects(#[from] acyclic_objects::LocalObjectsError),
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[error("local filesystem Objects bucket initialization failed: {0}")]
LocalObjectsBucket(acyclic_objects::ObjectsError),
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[error("local filesystem root setup failed: {0}")]
LocalRoot(std::io::Error),
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[error("local filesystem root is already open with different options")]
LocalOptionsConflict,
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[error("local filesystem initialization worker is unavailable")]
LocalInitializationWorker,
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
#[error(transparent)]
ObjectCacheConfig(#[from] crate::cache::ObjectCacheConfigError),
#[error(transparent)]
CheckoutMode(#[from] CheckoutModeError),
#[error("durable volume requested from non-durable embedded backends")]
UnsupportedDurability,
#[error("name collides with an existing entry under the volume's case-folding policy")]
NameCollision,
#[error("file identity collides with different authenticated content")]
FileIdentityCollision,
#[error("name is not normalized to NFC as required by the volume's unicode policy")]
NonNormalizedName,
#[error("checkout behavior is not implemented by this facade")]
UnsupportedCheckout,
#[error("direct-live mutation requires serialized-authority concurrency")]
LiveRequiresSerializedAuthority,
#[error("exclusive writer ownership raced with authority head {actual:?}")]
ExclusiveWriterConflict {
actual: Head,
},
#[error("direct-live retry bound must be positive")]
ZeroLiveRetryLimit,
#[error(transparent)]
LiveRetry(#[from] LiveRetryError),
#[error("direct-live checkout already has a pending operation")]
PendingLiveMutation,
#[error("live advancement conflicts with an observed or mutated region")]
LiveConflict {
conflicts: Vec<RebaseConflict>,
truncated: bool,
},
#[error(transparent)]
Authority(#[from] AuthorityStoreError),
#[error(transparent)]
Object(#[from] ObjectStoreError),
#[error(transparent)]
Closure(#[from] ClosureError),
#[error(transparent)]
Decode(#[from] CanonicalDecodeError),
#[error(transparent)]
VolumeCreated(#[from] VolumeCreatedError),
#[error(transparent)]
RetentionCreated(#[from] RetentionCreatedError),
#[error(transparent)]
Path(#[from] PathLookupError),
#[error(transparent)]
Directory(#[from] DirectoryReadError),
#[error(transparent)]
FileRecord(#[from] FileRecordReadError),
#[error(transparent)]
FileRead(#[from] FileRangeReadError),
#[error(transparent)]
BlobBuild(#[from] BlobBuildError),
#[error(transparent)]
BlobRead(#[from] BlobReadError),
#[error(transparent)]
AttributeLookup(#[from] AttributeLookupError),
#[error(transparent)]
AttributeList(#[from] AttributeListError),
#[error(transparent)]
AttributeMutation(#[from] AttributeMutationError),
#[error(transparent)]
InlineFile(#[from] InlineFileDataError),
#[error("filesystem path does not exist")]
NotFound,
#[error("filesystem path is not a directory")]
NotDirectory,
#[error("filesystem path is not a symbolic link")]
NotSymbolicLink,
#[error("filesystem path is not a Windows reparse point")]
NotReparsePoint,
#[error("filesystem special-file kind is incompatible with the requested payload")]
InvalidSpecialKind,
#[error("filesystem kind is unsupported by the configured volume profile")]
UnsupportedFileKind,
#[error("directory entry does not have a matching authenticated file record")]
InvalidDirectoryRecord,
#[error("generation export manifest does not match the imported closure")]
InvalidExportManifest,
#[error(transparent)]
Transfer(#[from] GenerationTransferError),
#[error(transparent)]
Dependency(#[from] DependencyError),
#[error(transparent)]
Probe(#[from] AuthenticatedProbeError),
#[error(transparent)]
Rebase(#[from] RebaseError<AuthenticatedProbeError>),
#[error(transparent)]
Mutation(#[from] GenerationMutationError),
#[error(transparent)]
DetachedMutation(#[from] RegularMutationError),
#[error(transparent)]
Checkpoint(#[from] CheckpointError),
#[error(transparent)]
Publication(#[from] PublicationError),
#[error("volume authority has no creation fact")]
EmptyAuthority,
#[error("volume authority history has an invalid event shape")]
InvalidAuthorityHistory,
#[error("authenticated data belongs to another volume")]
VolumeMismatch,
#[error("volume creation was rejected by authority state")]
CreationRejected,
#[error("workspace is deleted")]
WorkspaceDeleted,
#[error("writable private checkout requires a head selector")]
WritableCheckoutRequiresHead,
#[error("checkout does not admit private overlay mutation")]
MutationNotAllowed,
#[error("checkout has no pending mutation to commit")]
NoPendingMutations,
#[error("checkout does not admit explicit head refresh")]
RefreshNotAllowed,
#[error("pending overlay mutations require safe rebase before refresh")]
PendingMutationsRequireRebase,
#[error("checkout pending mutation count exceeds the volume limit")]
TooManyPendingMutations,
#[error("mutation input capacity exceeds the volume limit")]
ExcessiveMutationCapacity,
#[error("pending mutation replay log allocation failed")]
PendingMutationAllocationFailed,
#[error("prepared merge must be committed or discarded before further mutation")]
PreparedMergePending,
#[error("generation diff structure is invalid")]
InvalidDiff,
#[error("generation diff allocation failed")]
DiffAllocationFailed,
#[error("local garbage-collection live-set allocation failed")]
GarbageCollectionAllocationFailed,
#[error("three-way merge exceeds its change bound")]
MergeChangeLimit,
#[error("merge second parent must be the current authority head")]
MergeParentNotHead,
#[error(transparent)]
MergeFileTable(#[from] FileTableMutationError),
#[error(transparent)]
MergeTree(#[from] TreeMutationError),
#[error(transparent)]
Work(#[from] WorkError),
}
impl<A, O> Fs<A, O> {
#[must_use]
pub fn new(authority: A, objects: O, capabilities: EmbeddedCapabilities) -> Self {
Self::new_in_namespace(authority, objects, capabilities, [0; 16])
}
#[must_use]
pub fn new_in_namespace(
authority: A,
objects: O,
capabilities: EmbeddedCapabilities,
workspace_namespace: [u8; 16],
) -> Self {
Self::new_with_path_index(
authority,
objects,
capabilities,
workspace_namespace,
Arc::new(crate::path_index::MemoryGenerationPathIndex::default()),
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
None,
)
}
fn new_with_path_index(
authority: A,
objects: O,
capabilities: EmbeddedCapabilities,
workspace_namespace: [u8; 16],
path_index: Arc<dyn crate::path_index::GenerationPathIndex>,
#[cfg(all(feature = "local", not(target_arch = "wasm32")))] local_root_lifecycle: Option<
OwnershipAnchor,
>,
) -> Self {
Self {
inner: Arc::new(FsInner {
authority,
objects,
capabilities,
workspace_namespace,
path_index,
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
_local_root_lifecycle: local_root_lifecycle,
}),
}
}
}
impl<A, S> Fs<A, crate::cache::CachedObjectStore<S>> {
pub fn object_cache_stats(&self) -> Result<crate::cache::ObjectCacheStats, ObjectStoreError> {
self.inner.objects.stats()
}
pub fn clear_object_cache(&self) -> Result<(), ObjectStoreError> {
self.inner.objects.clear()
}
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
impl Fs<LocalAuthorityBackend, LocalObjectBackend> {
pub fn object_cache_stats(&self) -> Result<crate::cache::ObjectCacheStats, ObjectStoreError> {
self.inner.objects.inner().stats()
}
pub fn clear_object_cache(&self) -> Result<(), ObjectStoreError> {
self.inner.objects.inner().clear()
}
}
#[cfg(all(feature = "memory", feature = "distributed"))]
impl
Fs<
crate::distributed::StreamAuthorityStore<acyclic_stream::MemoryStream>,
crate::distributed::ProviderObjectStore<acyclic_objects::MemoryObjects>,
>
{
#[must_use]
pub fn memory() -> Self {
let stream = std::sync::Arc::new(acyclic_stream::MemoryStream::default());
let (objects, bucket) = acyclic_objects::MemoryObjects::with_default_bucket();
Self::from_memory_providers(stream, std::sync::Arc::new(objects), bucket)
}
#[must_use]
pub fn from_memory_providers(
stream: std::sync::Arc<acyclic_stream::MemoryStream>,
objects: std::sync::Arc<acyclic_objects::MemoryObjects>,
bucket: acyclic_objects::wire::BucketRef,
) -> Self {
Self::new(
crate::distributed::StreamAuthorityStore::new(stream),
crate::distributed::ProviderObjectStore::new(objects, bucket),
EmbeddedCapabilities::MEMORY,
)
}
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
impl Fs<LocalAuthorityBackend, LocalObjectBackend> {
#[doc(hidden)]
#[must_use]
pub fn local_root_release_barrier(
&self,
) -> Option<acyclic_native_runtime::OwnershipReleaseBarrier> {
self.inner
._local_root_lifecycle
.as_ref()
.map(OwnershipAnchor::release_barrier)
}
pub async fn local(options: LocalOptions) -> Result<Self, FsError> {
static REGISTRY: OnceLock<tokio::sync::Mutex<LocalRootRegistry>> = OnceLock::new();
let requested_root = options.root.clone();
let root = tokio::task::spawn_blocking(move || {
std::fs::create_dir_all(&requested_root)?;
std::fs::canonicalize(requested_root)
})
.await
.map_err(|_| FsError::LocalInitializationWorker)?
.map_err(FsError::LocalRoot)?;
let mut options = options;
options.root = root.clone();
let registry = REGISTRY.get_or_init(|| tokio::sync::Mutex::new(LocalRootRegistry::new()));
let opening = {
let mut registrations = registry.lock().await;
if let Some(existing) = registrations.get(&root)
&& let Some(inner) = existing.live.upgrade()
{
if existing.options != options {
return Err(FsError::LocalOptionsConflict);
}
return Ok(Self { inner });
}
prepare_local_root_open(&mut registrations, &root, &options)
};
let _opening = opening.lock().await;
let lifecycle = {
let registrations = registry.lock().await;
let existing = registrations
.get(&root)
.ok_or(FsError::LocalInitializationWorker)?;
if let Some(inner) = existing.live.upgrade() {
if existing.options != options {
return Err(FsError::LocalOptionsConflict);
}
return Ok(Self { inner });
}
Arc::clone(&existing.lifecycle)
};
let ownership = lifecycle.lock_owned().await;
let open_options = options.clone();
let fs = run_local_initialization(ownership, move |ownership| async move {
let ownership = OwnershipAnchor::new(ownership);
Self::open_local_unshared(open_options, Some(ownership)).await
})
.await??;
let mut registrations = registry.lock().await;
let existing = registrations
.get_mut(&root)
.ok_or(FsError::LocalInitializationWorker)?;
existing.options = options;
existing.live = Arc::downgrade(&fs.inner);
Ok(fs)
}
async fn open_local_unshared(
options: LocalOptions,
lifecycle: Option<OwnershipAnchor>,
) -> Result<Self, FsError> {
let LocalOptions {
root,
stream,
objects,
object_cache,
} = options;
let stream = std::sync::Arc::new(match &lifecycle {
Some(ownership) => {
acyclic_stream::LocalStream::open_with_ownership_anchor(
root.join("stream"),
stream,
ownership.clone(),
)
.await?
}
None => acyclic_stream::LocalStream::open(root.join("stream"), stream).await?,
});
let objects = std::sync::Arc::new(match &lifecycle {
Some(ownership) => {
acyclic_objects::LocalObjects::open_with_ownership_anchor(
root.join("objects"),
objects,
ownership.clone(),
)
.await?
}
None => acyclic_objects::LocalObjects::open(root.join("objects"), objects).await?,
});
let bucket = match objects
.bucket_named("filesystem-objects")
.await
.map_err(FsError::LocalObjectsBucket)?
{
Some(bucket) => bucket,
None => objects
.create_bucket(
"filesystem-objects".to_owned(),
Some("filesystem-objects-v1".to_owned()),
)
.await
.map_err(FsError::LocalObjectsBucket)?
.bucket
.ok_or(FsError::LocalObjectsBucket(
acyclic_objects::ObjectsError::Unavailable,
))?,
};
let objects = crate::cache::CachedObjectStore::new(
crate::distributed::ProviderObjectStore::new(objects, bucket),
object_cache,
)?;
let objects = crate::staged_objects::StagedObjects::new(objects);
Ok(Self::new_with_path_index(
crate::distributed::StreamAuthorityStore::new(stream),
objects,
EmbeddedCapabilities {
durable: cfg!(any(unix, windows)),
},
[0; 16],
Arc::new(crate::path_index::NativeGenerationPathIndex::new(
root.join("generation-path-index-v1"),
)),
lifecycle,
))
}
pub async fn collect_local_garbage(
options: LocalOptions,
maximum_authorities: u32,
maximum_candidates: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<LocalGarbageCollection> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
let fs = Self::open_local_unshared(options, None)
.await
.map_err(OperationFailure::before_work)?;
fs.collect_local_garbage_exclusive(
maximum_authorities,
maximum_candidates,
budget,
cancellation,
)
.await
}
async fn collect_local_garbage_exclusive(
&self,
maximum_authorities: u32,
maximum_candidates: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<LocalGarbageCollection> {
let authorities = self
.inner
.authority
.authorities(maximum_authorities)
.await
.map_err(|error| OperationFailure::before_work(error.into()))?;
let authority_live_bytes = u64::try_from(authorities.capacity())
.unwrap_or(u64::MAX)
.saturating_mul(
u64::try_from(size_of::<crate::foundation::AuthorityId>()).unwrap_or(u64::MAX),
);
let mut work = WorkCounters {
backend_read_operations: 1,
items_examined: u64::try_from(authorities.len()).unwrap_or(u64::MAX),
items_returned: u64::try_from(authorities.len()).unwrap_or(u64::MAX),
allocation_operations: u64::from(!authorities.is_empty()),
peak_allocation_bytes: authority_live_bytes,
..WorkCounters::default()
};
work.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let mut reachable = Vec::<ObjectId>::new();
for authority_id in authorities {
let Some((generation_root, config, next_work)) = self
.local_authority_generation(
authority_id,
authority_live_bytes,
work,
budget,
cancellation,
)
.await?
else {
continue;
};
work = next_work;
let live_bytes = authority_live_bytes.saturating_add(object_vec_bytes(&reachable));
let proof = prove_generation_closure_async(
&self.inner.objects,
generation_root,
closure_limits(config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = merge_simultaneous_work(work, proof.work, live_bytes, budget)?;
let incoming_bytes = object_vec_bytes(&proof.objects);
merge_sorted_object_ids(
&mut reachable,
&proof.objects,
incoming_bytes,
authority_live_bytes,
&mut work,
budget,
)?;
}
#[cfg(feature = "s3-http")]
{
work = self
.merge_active_s3_multipart_reachable_objects(
maximum_candidates,
authority_live_bytes,
&mut reachable,
work,
budget,
cancellation,
)
.await?;
}
let collected = self
.collect_unreachable_local_objects(
&reachable,
maximum_candidates,
authority_live_bytes,
&mut work,
budget,
cancellation,
)
.await?;
Ok(FsReceipt {
value: collected,
work,
})
}
#[cfg(feature = "s3-http")]
async fn merge_active_s3_multipart_reachable_objects(
&self,
maximum_candidates: u64,
authority_live_bytes: u64,
reachable: &mut Vec<ObjectId>,
work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<WorkCounters, OperationFailure<FsError>> {
let multipart = crate::active_s3_multipart_objects(
self.inner.authority.provider().as_ref(),
&self.inner.objects,
crate::FilesystemS3Limits::default(),
crate::S3MultipartRetentionLimits {
maximum_uploads: maximum_candidates.max(1),
..crate::S3MultipartRetentionLimits::default()
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|_| {
OperationFailure::new(
FsError::Object(ObjectStoreError::Rejected(
"active S3 multipart retention scan failed".to_owned(),
)),
work,
)
})?;
let mut work = merge_simultaneous_work(
work,
multipart.work,
authority_live_bytes.saturating_add(object_vec_bytes(reachable)),
budget,
)?;
let incoming_bytes = object_vec_bytes(&multipart.value);
merge_sorted_object_ids(
reachable,
&multipart.value,
incoming_bytes,
authority_live_bytes,
&mut work,
budget,
)?;
Ok(work)
}
async fn collect_unreachable_local_objects(
&self,
reachable: &[ObjectId],
maximum_candidates: u64,
retained_bytes: u64,
work: &mut WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<LocalGarbageCollection, OperationFailure<FsError>> {
if maximum_candidates == 0 {
return Err(OperationFailure::new(
FsError::Object(ObjectStoreError::Rejected(
"garbage-collection candidate bound must be positive".to_owned(),
)),
*work,
));
}
let (candidates, examined) = self
.list_unreachable_object_candidates(
reachable,
maximum_candidates,
retained_bytes,
work,
budget,
cancellation,
)
.await?;
let provider = self.inner.objects.inner().inner().provider();
let bucket = self.inner.objects.inner().inner().bucket().clone();
let mut removed = 0_u64;
for (object_key, version_id) in candidates {
cancellation
.check()
.map_err(|error| OperationFailure::new(error.into(), *work))?;
let identity = blake3::hash(format!("{object_key}\0{version_id}").as_bytes());
provider
.delete(
bucket.clone(),
object_key,
Some(version_id),
None,
Some(format!("fs-gc-{}", identity.to_hex())),
)
.await
.map_err(|error| {
OperationFailure::new(FsError::LocalObjectsBucket(error), *work)
})?;
removed = removed.saturating_add(1);
*work = work
.checked_add(WorkCounters {
backend_write_operations: 1,
items_examined: 1,
..WorkCounters::default()
})
.map_err(|error| OperationFailure::new(error.into(), *work))?;
work.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), *work))?;
}
let physical = provider
.collect_garbage(maximum_candidates)
.await
.map_err(|error| OperationFailure::new(FsError::LocalObjects(error), *work))?;
Ok(LocalGarbageCollection {
examined,
removed,
segments_removed: physical.segments_removed,
temporary_files_removed: physical.temporary_files_removed,
})
}
async fn list_unreachable_object_candidates(
&self,
reachable: &[ObjectId],
maximum_candidates: u64,
retained_bytes: u64,
work: &mut WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(Vec<(String, String)>, u64), OperationFailure<FsError>> {
let reachable_keys = reachable
.iter()
.copied()
.map(crate::distributed::object_key)
.collect::<BTreeSet<_>>();
let provider = self.inner.objects.inner().inner().provider();
let bucket = self.inner.objects.inner().inner().bucket().clone();
let mut continuation = None;
let mut candidates = Vec::new();
let mut examined = 0_u64;
loop {
cancellation
.check()
.map_err(|error| OperationFailure::new(error.into(), *work))?;
let remaining_candidates = maximum_candidates.saturating_sub(examined);
if remaining_candidates == 0 && continuation.is_some() {
return Err(OperationFailure::new(
FsError::Object(ObjectStoreError::Rejected(
"garbage-collection candidate bound exceeded".to_owned(),
)),
*work,
));
}
let page_size = u32::try_from(remaining_candidates.min(256)).unwrap_or(256);
let page = provider
.list(
acyclic_objects::ReadTarget::Bucket(bucket.clone()),
"fs/v1/".to_owned(),
None,
true,
page_size,
continuation,
)
.await
.map_err(|error| {
OperationFailure::new(FsError::LocalObjectsBucket(error), *work)
})?;
let page_items = u64::try_from(page.entries.len()).unwrap_or(u64::MAX);
examined = examined
.checked_add(page_items)
.ok_or_else(|| OperationFailure::new(FsError::Work(WorkError::Overflow), *work))?;
if examined > maximum_candidates {
return Err(OperationFailure::new(
FsError::Object(ObjectStoreError::Rejected(
"garbage-collection candidate bound exceeded".to_owned(),
)),
*work,
));
}
for entry in page.entries {
let version = entry.version.ok_or_else(|| {
OperationFailure::new(FsError::Object(ObjectStoreError::Corrupt), *work)
})?;
if !reachable_keys.contains(&entry.object_key) {
candidates.push((entry.object_key, version.version_id));
}
}
*work = work
.checked_add(WorkCounters {
backend_read_operations: 1,
items_examined: page_items,
allocation_operations: u64::from(!candidates.is_empty()),
peak_allocation_bytes: retained_bytes
.saturating_add(object_slice_bytes(reachable)),
..WorkCounters::default()
})
.map_err(|error| OperationFailure::new(error.into(), *work))?;
work.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), *work))?;
continuation = page.continuation;
if continuation.is_none() {
break;
}
}
Ok((candidates, examined))
}
async fn local_authority_generation(
&self,
authority_id: crate::foundation::AuthorityId,
retained_bytes: u64,
mut work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<Option<(ObjectId, VolumeConfig, WorkCounters)>, OperationFailure<FsError>> {
cancellation
.check()
.map_err(|error| OperationFailure::new(error.into(), work))?;
let head = self
.inner
.authority
.head(authority_id, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = merge_simultaneous_work(work, head.work, retained_bytes, budget)?;
if head.value.sequence == Sequence::GENESIS {
return Ok(None);
}
let creation = self
.inner
.authority
.replay(
authority_id,
Sequence::GENESIS,
ReplayLimit {
records: 1,
payload_bytes: MAXIMUM_VOLUME_EVENT_BYTES,
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = merge_simultaneous_work(work, creation.work, retained_bytes, budget)?;
let first = creation
.value
.first()
.ok_or_else(|| OperationFailure::new(FsError::InvalidAuthorityHistory, work))?;
let Ok(created) = decode_volume_created(&first.payload, MAXIMUM_VOLUME_EVENT_BYTES) else {
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
if let Ok(source_volume) =
decode_source_volume(&first.payload, MAXIMUM_VOLUME_EVENT_BYTES)
{
if source_authority_id(source_volume) != authority_id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
return Ok(None);
}
let retained = decode_retention_created(&first.payload, MAXIMUM_VOLUME_EVENT_BYTES)
.map_err(|error| OperationFailure::new(error.into(), work))?;
return Ok(Some((retained.generation_root, retained.config, work)));
};
if volume_authority_id(created.volume_id) != authority_id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
let generation_root = if head.value.sequence == Sequence::new(1) {
created.initial_generation_root
} else {
let latest = self
.inner
.authority
.replay(
authority_id,
Sequence::new(head.value.sequence.get().saturating_sub(1)),
ReplayLimit {
records: 1,
payload_bytes: MAXIMUM_VOLUME_EVENT_BYTES,
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = merge_simultaneous_work(work, latest.work, retained_bytes, budget)?;
let record = latest
.value
.first()
.ok_or_else(|| OperationFailure::new(FsError::InvalidAuthorityHistory, work))?;
if let Ok(deleted) =
decode_workspace_deleted(&record.payload, MAXIMUM_VOLUME_EVENT_BYTES)
{
if deleted != created.volume_id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
return Ok(None);
}
generation_from_record(record, created.volume_id, work)?
};
Ok(Some((generation_root, created.config, work)))
}
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Fs<A, O> {
pub(crate) async fn observe_volume_operation(
&self,
volume_id: VolumeId,
operation_id: OperationId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<DurableCommit>> {
let receipt = self
.inner
.authority
.find_operation(
volume_authority_id(volume_id),
operation_id,
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(FsError::Authority(failure.error), *failure.work)
})?;
Ok(FsReceipt {
value: receipt.value,
work: receipt.work,
})
}
pub(crate) async fn generation_parents(
&self,
volume: &Volume<A, O>,
generation: GenerationId,
) -> Result<Vec<GenerationId>, crate::workspace::WorkspaceError> {
let object = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: generation.digest(),
};
let (root, _) = read_generation_root(
&self.inner.objects,
object,
volume.config,
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if root.volume_id != volume.id {
return Err(crate::workspace::WorkspaceError::ForeignGeneration);
}
Ok(root.parents)
}
pub async fn create_workspace(
&self,
name: impl AsRef<str>,
) -> Result<crate::Workspace<A, O>, crate::workspace::WorkspaceError> {
let lifecycle = if self.inner.capabilities.durable {
Lifecycle::Durable
} else {
Lifecycle::Ephemeral
};
self.create_workspace_with_config(name, VolumeConfig::portable(lifecycle))
.await
}
pub async fn create_workspace_with_config(
&self,
name: impl AsRef<str>,
config: VolumeConfig,
) -> Result<crate::Workspace<A, O>, crate::workspace::WorkspaceError> {
self.create_workspace_with_config_operation(name, config, None)
.await
}
pub(crate) async fn create_workspace_with_config_operation(
&self,
name: impl AsRef<str>,
config: VolumeConfig,
operation_id: Option<OperationId>,
) -> Result<crate::Workspace<A, O>, crate::workspace::WorkspaceError> {
let name = crate::WorkspaceName::new(name)?;
let id = crate::WorkspaceId::derive(self.inner.workspace_namespace, &name);
let volume = self
.create_volume_with_id_operation(
id.volume_id(),
config,
operation_id,
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?
.value;
volume
.resolve_head_generation(WorkBudget::UNBOUNDED, &CancellationToken::new())
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(crate::Workspace {
name,
id,
volume,
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
source: None,
})
}
pub async fn open_workspace(
&self,
name: impl AsRef<str>,
) -> Result<crate::Workspace<A, O>, crate::workspace::WorkspaceError> {
let name = crate::WorkspaceName::new(name)?;
let id = crate::WorkspaceId::derive(self.inner.workspace_namespace, &name);
let volume = self
.open_volume(
id.volume_id(),
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?
.value;
volume
.resolve_head_generation(WorkBudget::UNBOUNDED, &CancellationToken::new())
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(crate::Workspace {
name,
id,
volume,
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
source: None,
})
}
pub async fn open_volume_workspace(
&self,
name: impl AsRef<str>,
volume_id: VolumeId,
) -> Result<crate::Workspace<A, O>, crate::workspace::WorkspaceError> {
let name = crate::WorkspaceName::new(name)?;
let volume = self
.open_volume(volume_id, WorkBudget::UNBOUNDED, &CancellationToken::new())
.await
.map_err(crate::workspace::WorkspaceError::engine)?
.value;
volume
.resolve_head_generation(WorkBudget::UNBOUNDED, &CancellationToken::new())
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(crate::Workspace {
name,
id: crate::WorkspaceId::from_volume_id(volume_id),
volume,
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
source: None,
})
}
pub fn adopt_volume_workspace(
&self,
name: impl AsRef<str>,
volume: Volume<A, O>,
) -> Result<crate::Workspace<A, O>, crate::workspace::WorkspaceError> {
let name = crate::WorkspaceName::new(name)?;
let id = crate::WorkspaceId::from_volume_id(volume.id);
Ok(crate::Workspace {
name,
id,
volume,
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
source: None,
})
}
pub async fn delete_workspace(
&self,
name: impl AsRef<str>,
idempotency_key: crate::IdempotencyKey,
) -> Result<crate::workspace::WorkspaceDelete, crate::workspace::WorkspaceError> {
let name = crate::WorkspaceName::new(name)?;
let id = crate::WorkspaceId::derive(self.inner.workspace_namespace, &name);
let volume = self
.open_volume(
id.volume_id(),
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?
.value;
self.delete_workspace_volume(&volume, idempotency_key.operation_id())
.await
}
async fn materialize_forked_generation_root(
&self,
source: &crate::Generation<A, O>,
destination_id: crate::WorkspaceId,
config: VolumeConfig,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(ObjectId, VerifiedForkSource, WorkCounters), crate::workspace::WorkspaceError>
{
let source_object = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: source.id.digest(),
};
let (source_root, work) = read_generation_root(
&self.inner.objects,
source_object,
config,
budget,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if source_root.volume_id != source.workspace.volume.id {
return Err(crate::workspace::WorkspaceError::ForeignGeneration);
}
let fork_root = GenerationRoot {
volume_id: destination_id.volume_id(),
root_file_id: source_root.root_file_id,
file_table: source_root.file_table,
parents: vec![source.id],
required_features: source_root.required_features,
};
let encoded =
encode_generation_root(&fork_root).map_err(crate::workspace::WorkspaceError::engine)?;
let (generation_root, work) = self
.put_encoded(
ObjectKind::GenerationRoot,
encoded,
work,
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let proof = prove_generation_closure_async(
&self.inner.objects,
generation_root,
closure_limits(config),
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let work = add(work, proof.work).map_err(crate::workspace::WorkspaceError::engine)?;
Ok((
generation_root,
VerifiedForkSource {
generation_root: source_object,
},
work,
))
}
pub(crate) async fn fork_workspace_measured(
&self,
destination: crate::WorkspaceName,
source: &crate::Generation<A, O>,
idempotency_key: crate::IdempotencyKey,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<OperationReceipt<crate::Workspace<A, O>>, crate::workspace::WorkspaceError> {
if !Arc::ptr_eq(&self.inner, &source.workspace.volume.fs.inner) {
return Err(crate::workspace::WorkspaceError::ForeignGeneration);
}
cancellation
.check()
.map_err(crate::workspace::WorkspaceError::from)?;
let destination_id =
crate::WorkspaceId::derive(self.inner.workspace_namespace, &destination);
let config = source.workspace.volume.config;
let (generation_root, verified_source, mut work) =
Box::pin(self.materialize_forked_generation_root(
source,
destination_id,
config,
budget,
cancellation,
))
.await?;
let retained = Box::pin(self.retain_verified_workspace_generation_measured(
&source.workspace.volume,
verified_source,
RetentionKind::ForkBase,
hex::encode(destination_id.into_bytes()),
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
))
.await?;
work = add(work, retained).map_err(crate::workspace::WorkspaceError::engine)?;
if self.inner.authority.supports_generation_lineage_prefix() {
let head = Box::pin(source.workspace.head_measured(
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
))
.await?;
work = add(work, head.work).map_err(crate::workspace::WorkspaceError::engine)?;
let lineage = if head.value.id == source.id {
crate::GenerationFork::PublishedPrefix
} else {
crate::GenerationFork::Independent
};
let forked = Box::pin(self.inner.authority.fork_generation_authority(
crate::GenerationForkSource {
authority: volume_authority_id(source.workspace.volume.id),
generation: source.id,
lineage,
},
volume_authority_id(destination_id.volume_id()),
idempotency_key.operation_id(),
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
))
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
work = add(work, forked.work).map_err(crate::workspace::WorkspaceError::engine)?;
}
let volume = Box::pin(self.publish_volume_creation(
VolumeCreation {
volume_id: destination_id.volume_id(),
config,
generation_root,
operation_id: Some(idempotency_key.operation_id()),
},
work,
budget,
cancellation,
))
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
work = volume.work;
let volume = volume.value;
let resolved = Box::pin(volume.resolve_head_generation(
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
))
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
work = add(work, resolved.2).map_err(crate::workspace::WorkspaceError::engine)?;
Ok(OperationReceipt {
value: crate::Workspace {
name: destination,
id: destination_id,
volume,
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
source: None,
},
work,
})
}
pub(crate) async fn retain_workspace_generation(
&self,
volume: &Volume<A, O>,
generation_id: GenerationId,
kind: RetentionKind,
label: String,
) -> Result<(), crate::workspace::WorkspaceError> {
let cancellation = CancellationToken::new();
let generation_root = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: generation_id.digest(),
};
let proof = prove_generation_closure_async(
&self.inner.objects,
generation_root,
closure_limits(volume.config),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if proof.root.volume_id != volume.id {
return Err(crate::workspace::WorkspaceError::ForeignGeneration);
}
self.retain_verified_workspace_generation(
volume,
VerifiedForkSource { generation_root },
kind,
label,
&cancellation,
)
.await
}
async fn retain_verified_workspace_generation(
&self,
volume: &Volume<A, O>,
source: VerifiedForkSource,
kind: RetentionKind,
label: String,
cancellation: &CancellationToken,
) -> Result<(), crate::workspace::WorkspaceError> {
self.retain_verified_workspace_generation_measured(
volume,
source,
kind,
label,
WorkBudget::UNBOUNDED,
cancellation,
)
.await
.map(|_| ())
}
async fn retain_verified_workspace_generation_measured(
&self,
volume: &Volume<A, O>,
source: VerifiedForkSource,
kind: RetentionKind,
label: String,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<WorkCounters, crate::workspace::WorkspaceError> {
let generation_root = source.generation_root;
let authority_id = retention_authority_id(volume.id, kind, &label);
let created = self
.inner
.authority
.create_authority(authority_id, Epoch::GENESIS, budget, cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let mut work = created.work;
let active_head = match created.value {
CreateAuthorityOutcome::Created(head) | CreateAuthorityOutcome::Existing(head) => head,
};
let payload = encode_retention_created(&RetentionCreated {
volume_id: volume.id,
kind,
label,
generation_root,
config: volume.config,
})
.map_err(crate::workspace::WorkspaceError::engine)?;
let operation_id = OperationId::from_bytes(authority_id.into_bytes());
let (commit, _) = creation_commit(operation_id, payload);
let drained = self
.inner
.objects
.flush_before_publish(
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
work = add(work, drained.work).map_err(crate::workspace::WorkspaceError::engine)?;
let appended = self
.inner
.authority
.compare_and_append(
authority_id,
active_head.epoch,
Head::genesis(active_head.epoch),
commit,
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
work = add(work, appended.work).map_err(crate::workspace::WorkspaceError::engine)?;
match appended.value {
AppendOutcome::Committed(_) | AppendOutcome::AlreadyCommitted(_) => Ok(work),
AppendOutcome::Conflict { .. }
| AppendOutcome::Fenced { .. }
| AppendOutcome::IdempotencyConflict { .. } => {
Err(crate::workspace::WorkspaceError::RetentionConflict)
}
}
}
pub(crate) async fn delete_workspace_volume(
&self,
volume: &Volume<A, O>,
operation_id: OperationId,
) -> Result<crate::workspace::WorkspaceDelete, crate::workspace::WorkspaceError> {
let cancellation = CancellationToken::new();
let authority_id = volume_authority_id(volume.id);
let head = self
.inner
.authority
.head(authority_id, WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?
.value;
if head.sequence == Sequence::GENESIS {
return Err(crate::workspace::WorkspaceError::engine(
FsError::EmptyAuthority,
));
}
let latest = self
.inner
.authority
.replay(
authority_id,
Sequence::new(head.sequence.get().saturating_sub(1)),
ReplayLimit {
records: 1,
payload_bytes: MAXIMUM_VOLUME_EVENT_BYTES,
},
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let latest = latest.value.first().ok_or_else(|| {
crate::workspace::WorkspaceError::engine(FsError::InvalidAuthorityHistory)
})?;
if let Ok(deleted) = decode_workspace_deleted(&latest.payload, MAXIMUM_VOLUME_EVENT_BYTES) {
if deleted != volume.id {
return Err(crate::workspace::WorkspaceError::engine(
FsError::VolumeMismatch,
));
}
return Ok(crate::workspace::WorkspaceDelete::AlreadyDeleted);
}
generation_from_record(latest, volume.id, WorkCounters::default())
.map_err(crate::workspace::WorkspaceError::engine)?;
let (commit, _) = creation_commit(operation_id, encode_workspace_deleted(volume.id));
let appended = self
.inner
.authority
.compare_and_append(
authority_id,
head.epoch,
head,
commit,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(match appended.value {
AppendOutcome::Committed(_) | AppendOutcome::AlreadyCommitted(_) => {
crate::workspace::WorkspaceDelete::Deleted
}
AppendOutcome::Conflict { .. } | AppendOutcome::Fenced { .. } => {
crate::workspace::WorkspaceDelete::Conflict
}
AppendOutcome::IdempotencyConflict { .. } => {
crate::workspace::WorkspaceDelete::IdempotencyConflict
}
})
}
pub(crate) async fn workspace_common_ancestor(
&self,
source: &crate::Generation<A, O>,
target: &crate::Generation<A, O>,
maximum_generations: u32,
) -> Result<WorkspaceCommonAncestor, crate::workspace::WorkspaceError> {
if maximum_generations == 0
|| !Arc::ptr_eq(&self.inner, &source.workspace.volume.fs.inner)
|| !Arc::ptr_eq(&self.inner, &target.workspace.volume.fs.inner)
{
return Err(crate::workspace::WorkspaceError::ForeignGeneration);
}
if source.workspace.volume.config != target.workspace.volume.config {
return Err(crate::workspace::WorkspaceError::IncompatibleWorkspace);
}
let cancellation = CancellationToken::new();
let (target_ancestors, target_truncated) = collect_generation_ancestors(
&self.inner.objects,
target.id,
target.workspace.volume.config,
maximum_generations,
&cancellation,
)
.await?;
find_first_generation_ancestor(
&self.inner.objects,
source.id,
source.workspace.volume.config,
maximum_generations,
&target_ancestors,
target_truncated,
&cancellation,
)
.await
}
pub(crate) async fn workspace_join_changes(
&self,
base: GenerationId,
source: &crate::Generation<A, O>,
maximum_changes: u32,
) -> Result<FsReceipt<GenerationDiff>, crate::workspace::WorkspaceError> {
self.workspace_join_changes_bounded(
base,
source,
maximum_changes,
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
}
pub(crate) async fn workspace_join_changes_bounded(
&self,
base: GenerationId,
source: &crate::Generation<A, O>,
maximum_changes: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<FsReceipt<GenerationDiff>, crate::workspace::WorkspaceError> {
if maximum_changes == 0 || !Arc::ptr_eq(&self.inner, &source.workspace.volume.fs.inner) {
return Err(crate::workspace::WorkspaceError::ForeignGeneration);
}
let config = source.workspace.volume.config;
let mut work = WorkCounters::default();
let (base_root, base_work) = read_generation_root(
&self.inner.objects,
ObjectId {
kind: ObjectKind::GenerationRoot,
digest: base.digest(),
},
config,
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
work = add(work, base_work).map_err(crate::workspace::WorkspaceError::engine)?;
let (source_root, source_work) = read_generation_root(
&self.inner.objects,
ObjectId {
kind: ObjectKind::GenerationRoot,
digest: source.id.digest(),
},
config,
remaining(work, budget).map_err(crate::workspace::WorkspaceError::engine)?,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
work = add(work, source_work).map_err(crate::workspace::WorkspaceError::engine)?;
if source_root.volume_id != source.workspace.volume.id
|| base_root.root_file_id != source_root.root_file_id
{
return Err(crate::workspace::WorkspaceError::IncompatibleWorkspace);
}
let receipt = diff_generation_file_tables(
&self.inner.objects,
base_root.file_table,
source_root.file_table,
maximum_changes,
source.workspace.volume.config,
work,
budget,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if receipt.value.truncated {
return Err(crate::workspace::WorkspaceError::JoinLimit);
}
Ok(receipt)
}
pub(crate) async fn workspace_head_state(
&self,
workspace: &Volume<A, O>,
) -> Result<(GenerationId, Head), crate::workspace::WorkspaceError> {
let (root, head, _) = workspace
.resolve_head_generation(WorkBudget::UNBOUNDED, &CancellationToken::new())
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok((GenerationId::new(root.digest), head))
}
pub(crate) async fn workspace_operation_generation(
&self,
workspace: &Volume<A, O>,
operation_id: OperationId,
) -> Result<Option<GenerationId>, crate::workspace::WorkspaceError> {
let cancellation = CancellationToken::new();
let observed = self
.observe_volume_operation(
workspace.id,
operation_id,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
observed
.value
.as_ref()
.map(|commit| {
generation_from_record(commit, workspace.id, observed.work)
.map(|object| GenerationId::new(object.digest))
.map_err(crate::workspace::WorkspaceError::engine)
})
.transpose()
}
pub(crate) async fn workspace_conflict_record(
&self,
workspace: &Volume<A, O>,
generation: GenerationId,
file_id: FileId,
) -> Result<Option<FileRecord>, crate::workspace::WorkspaceError> {
let cancellation = CancellationToken::new();
let generation_root = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: generation.digest(),
};
let (root, work) = read_generation_root(
&self.inner.objects,
generation_root,
workspace.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let lookup = lookup_file_record_async(
&self.inner.objects,
root.file_table,
file_id,
decode_limits(workspace.config),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let _ = work;
Ok(lookup.record)
}
pub(crate) async fn workspace_conflict_bytes(
&self,
workspace: &Volume<A, O>,
record: FileRecord,
) -> Result<Option<Bytes>, crate::workspace::WorkspaceError> {
let logical_bytes = match record.payload {
FilePayload::InlineRegular(data) => {
return Ok(Some(Bytes::copy_from_slice(data.as_bytes())));
}
FilePayload::Regular { logical_bytes, .. } => logical_bytes,
_ => return Ok(None),
};
if logical_bytes > workspace.config.limits.maximum_read_bytes {
return Ok(None);
}
let read = read_file_range_async(
&self.inner.objects,
FileRangeRequest {
record,
range: ByteRange {
offset: 0,
length: logical_bytes,
},
maximum_spans: workspace.config.limits.maximum_directory_page_entries,
limits: decode_limits(workspace.config),
budget: WorkBudget::UNBOUNDED,
},
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(Some(read.bytes))
}
pub(crate) async fn workspace_conflict_metadata(
&self,
workspace: &Volume<A, O>,
record: FileRecord,
) -> Result<Vec<u8>, crate::workspace::WorkspaceError> {
let read = self
.inner
.objects
.read(
record.metadata,
workspace.config.limits.maximum_object_bytes,
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
decode_file_metadata(&read.value.bytes, decode_limits(workspace.config))
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(read.value.bytes.to_vec())
}
pub(crate) async fn stage_merge_regular_record(
&self,
workspace: &Volume<A, O>,
mut record: FileRecord,
bytes: Bytes,
) -> Result<FileRecord, crate::workspace::WorkspaceError> {
if record.kind != FileKind::Regular
|| u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> workspace.config.limits.maximum_read_bytes
{
return Err(crate::workspace::WorkspaceError::InvalidMergeResolution);
}
if bytes.len() <= crate::kernel::MAXIMUM_INLINE_FILE_BYTES {
record.payload = FilePayload::InlineRegular(
InlineFileData::new(&bytes).map_err(crate::workspace::WorkspaceError::engine)?,
);
return Ok(record);
}
let maximum_blob_bytes = u64::try_from(bytes.len()).unwrap_or(u64::MAX).max(1);
let mut source = std::io::Cursor::new(bytes);
let blob = build_blob_async(
&self.inner.objects,
&mut source,
BlobBuildOptions {
chunk_bytes: u32::try_from(
workspace
.config
.limits
.maximum_object_bytes
.min(1024 * 1024),
)
.unwrap_or(u32::MAX)
.max(1),
page_items: workspace.config.limits.maximum_directory_page_entries,
page_bytes: u32::try_from(workspace.config.limits.maximum_object_bytes)
.unwrap_or(u32::MAX)
.max(1),
maximum_blob_bytes,
},
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let payload = apply_regular_mutation_async(
&self.inner.objects,
FilePayload::InlineRegular(
InlineFileData::new(&[]).map_err(crate::workspace::WorkspaceError::engine)?,
),
RegularMutation::Write {
offset: 0,
length: blob.logical_bytes,
content: blob.root,
content_offset: 0,
},
workspace.config,
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
record.payload = payload.payload;
Ok(record)
}
pub(crate) async fn stage_merge_metadata(
&self,
workspace: &Volume<A, O>,
bytes: &[u8],
) -> Result<ObjectId, crate::workspace::WorkspaceError> {
decode_file_metadata(bytes, decode_limits(workspace.config))
.map_err(crate::workspace::WorkspaceError::engine)?;
let (object, _) = self
.put_encoded(
ObjectKind::Metadata,
bytes.to_vec(),
WorkCounters::default(),
WorkBudget::UNBOUNDED,
&CancellationToken::new(),
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(object)
}
pub(crate) async fn restore_workspace_generation(
&self,
workspace: &Volume<A, O>,
generation: GenerationId,
expected: GenerationId,
operation_id: OperationId,
permit: PublicationPermit,
) -> Result<crate::workspace::WorkspaceRestoreOutcome, crate::workspace::WorkspaceError> {
let cancellation = CancellationToken::new();
if let Some(recorded) = self
.workspace_operation_generation(workspace, operation_id)
.await?
{
return Ok(if recorded == generation {
crate::workspace::WorkspaceRestoreOutcome::AlreadyRestored(recorded)
} else {
crate::workspace::WorkspaceRestoreOutcome::IdempotencyConflict
});
}
let (current, authority_head, _) = workspace
.resolve_head_generation(WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let current = GenerationId::new(current.digest);
if current != expected {
return Ok(crate::workspace::WorkspaceRestoreOutcome::Stale(current));
}
if current == generation {
return Ok(crate::workspace::WorkspaceRestoreOutcome::Current(current));
}
let generation_root = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: generation.digest(),
};
let (root, _) = read_generation_root(
&self.inner.objects,
generation_root,
workspace.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if root.volume_id != workspace.id {
return Err(crate::workspace::WorkspaceError::ForeignGeneration);
}
let publication = publish_generation_async_with_permit(
&self.inner.objects,
&self.inner.authority,
PublishGenerationRequest {
authority_id: volume_authority_id(workspace.id),
volume_id: workspace.id,
epoch: authority_head.epoch,
expected: authority_head,
operation_id,
generation_root,
},
PublicationIntent::guarded(None, permit),
closure_limits(workspace.config),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(match publication.outcome {
AppendOutcome::Committed(_) => {
crate::workspace::WorkspaceRestoreOutcome::Restored(publication.proof.generation_id)
}
AppendOutcome::AlreadyCommitted(_) => {
crate::workspace::WorkspaceRestoreOutcome::AlreadyRestored(
publication.proof.generation_id,
)
}
AppendOutcome::Conflict { .. } => {
let (actual, _, _) = workspace
.resolve_head_generation(WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
crate::workspace::WorkspaceRestoreOutcome::Stale(GenerationId::new(actual.digest))
}
AppendOutcome::Fenced { .. } => crate::workspace::WorkspaceRestoreOutcome::Fenced,
AppendOutcome::IdempotencyConflict { .. } => {
crate::workspace::WorkspaceRestoreOutcome::IdempotencyConflict
}
})
}
#[allow(clippy::too_many_lines)]
pub(crate) async fn live_rebase_workspace(
&self,
request: WorkspaceRebaseRequest<'_, A, O>,
) -> Result<WorkspaceJoinOutcome, crate::workspace::WorkspaceError> {
let WorkspaceRebaseRequest {
target,
operation_id,
maximum_generations,
maximum_changes,
maximum_conflicts,
} = request;
if maximum_generations == 0 || maximum_changes == 0 || maximum_conflicts == 0 {
return Err(crate::workspace::WorkspaceError::JoinLimit);
}
let cancellation = CancellationToken::new();
let (target_object, expected_head, _) = target
.resolve_head_generation(WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let (target_root, _) = read_generation_root(
&self.inner.objects,
target_object,
target.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let mut cursor = target_object;
let mut adopted = None;
for _ in 0..maximum_generations {
let (root, _) = read_generation_root(
&self.inner.objects,
cursor,
target.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if root.volume_id != target.id {
adopted = Some((cursor, root));
break;
}
let Some(parent) = root.parents.first() else {
return Err(crate::workspace::WorkspaceError::NotFork);
};
cursor = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: parent.digest(),
};
}
let Some((base_object, base_root)) = adopted else {
return Err(crate::workspace::WorkspaceError::LineageLimit);
};
let source = self
.open_volume(base_root.volume_id, WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?
.value;
if source.config != target.config || base_root.root_file_id != target_root.root_file_id {
return Err(crate::workspace::WorkspaceError::IncompatibleWorkspace);
}
let (source_object, _, _) = source
.resolve_head_generation(WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if source_object == base_object {
return Ok(WorkspaceJoinOutcome::NoChanges(GenerationId::new(
target_object.digest,
)));
}
let (source_root, _) = read_generation_root(
&self.inner.objects,
source_object,
target.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if source_root.volume_id != source.id
|| source_root.root_file_id != target_root.root_file_id
{
return Err(crate::workspace::WorkspaceError::IncompatibleWorkspace);
}
let normalize = |root: &GenerationRoot| GenerationRoot {
volume_id: target.id,
root_file_id: target_root.root_file_id,
file_table: root.file_table,
parents: Vec::new(),
required_features: root.required_features,
};
let normalized_base = normalize(&base_root);
let normalized_source = normalize(&source_root);
let (normalized_base_object, _) = self
.put_encoded(
ObjectKind::GenerationRoot,
encode_generation_root(&normalized_base)
.map_err(crate::workspace::WorkspaceError::engine)?,
WorkCounters::default(),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let (normalized_source_object, _) = self
.put_encoded(
ObjectKind::GenerationRoot,
encode_generation_root(&normalized_source)
.map_err(crate::workspace::WorkspaceError::engine)?,
WorkCounters::default(),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let merged = merge_generation_async(
&self.inner.objects,
MergeGenerationRequest {
base_generation: normalized_base_object,
base: normalized_base,
ours_generation: Some(normalized_source_object),
ours: normalized_source,
theirs_generation: target_object,
theirs: target_root,
retain_theirs_parent: false,
maximum_changes,
maximum_conflicts,
resolutions: BTreeMap::new(),
},
decode_limits(target.config),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let merged_root = match merged.value {
MergeGenerationOutcome::Conflicted {
conflicts,
truncated,
} => return Ok(WorkspaceJoinOutcome::Conflicted(conflicts, truncated)),
MergeGenerationOutcome::Prepared { root, .. } => root,
};
let rebased_root = GenerationRoot {
volume_id: target.id,
root_file_id: merged_root.root_file_id,
file_table: merged_root.file_table,
parents: vec![GenerationId::new(source_object.digest)],
required_features: merged_root.required_features,
};
let (candidate, _) = self
.put_encoded(
ObjectKind::GenerationRoot,
encode_generation_root(&rebased_root)
.map_err(crate::workspace::WorkspaceError::engine)?,
WorkCounters::default(),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let publication = publish_generation_async(
&self.inner.objects,
&self.inner.authority,
PublishGenerationRequest {
authority_id: volume_authority_id(target.id),
volume_id: target.id,
epoch: expected_head.epoch,
expected: expected_head,
operation_id,
generation_root: candidate,
},
closure_limits(target.config),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(match publication.outcome {
AppendOutcome::Committed(_) => {
WorkspaceJoinOutcome::Applied(publication.proof.generation_id)
}
AppendOutcome::AlreadyCommitted(_) => {
WorkspaceJoinOutcome::AlreadyApplied(publication.proof.generation_id)
}
AppendOutcome::Conflict { .. } => {
let (actual, _, _) = target
.resolve_head_generation(WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
WorkspaceJoinOutcome::Stale(GenerationId::new(actual.digest))
}
AppendOutcome::Fenced { .. } => WorkspaceJoinOutcome::Fenced,
AppendOutcome::IdempotencyConflict { .. } => WorkspaceJoinOutcome::IdempotencyConflict,
})
}
#[allow(clippy::too_many_lines)]
pub(crate) async fn apply_workspace_join(
&self,
request: WorkspaceJoinRequest<'_, A, O>,
) -> Result<WorkspaceJoinOutcome, crate::workspace::WorkspaceError> {
let operation_context = request.operation_context();
let WorkspaceJoinRequest {
target,
base,
source,
expected_target,
expected_head,
history,
operation_id,
permit,
maximum_generations,
maximum_changes,
maximum_conflicts,
resolutions,
} = request;
if maximum_generations == 0 || maximum_changes == 0 || maximum_conflicts == 0 {
return Err(crate::workspace::WorkspaceError::JoinLimit);
}
if !Arc::ptr_eq(&self.inner, &source.workspace.volume.fs.inner)
|| source.workspace.volume.config != target.config
{
return Err(crate::workspace::WorkspaceError::IncompatibleWorkspace);
}
let cancellation = CancellationToken::new();
let (current_target_object, _, _) = target
.resolve_head_generation(WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let current_target = GenerationId::new(current_target_object.digest);
let target_object = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: expected_target.digest(),
};
let base_object = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: base.id.digest(),
};
let source_object = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: source.id.digest(),
};
let (base_root, _) = read_generation_root(
&self.inner.objects,
base_object,
target.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let (source_root, _) = read_generation_root(
&self.inner.objects,
source_object,
target.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let (target_root, _) = read_generation_root(
&self.inner.objects,
target_object,
target.config,
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
if source_root.volume_id != source.workspace.volume.id
|| target_root.volume_id != target.id
|| base_root.root_file_id != target_root.root_file_id
|| source_root.root_file_id != target_root.root_file_id
{
return Err(crate::workspace::WorkspaceError::IncompatibleWorkspace);
}
let normalized_base = GenerationRoot {
volume_id: target.id,
root_file_id: target_root.root_file_id,
file_table: base_root.file_table,
parents: base_root.parents,
required_features: base_root.required_features,
};
let normalized_source = GenerationRoot {
volume_id: target.id,
root_file_id: target_root.root_file_id,
file_table: source_root.file_table,
parents: source_root.parents,
required_features: source_root.required_features,
};
let (normalized_base_object, _) = self
.put_encoded(
ObjectKind::GenerationRoot,
encode_generation_root(&normalized_base)
.map_err(crate::workspace::WorkspaceError::engine)?,
WorkCounters::default(),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let (normalized_source_object, _) = self
.put_encoded(
ObjectKind::GenerationRoot,
encode_generation_root(&normalized_source)
.map_err(crate::workspace::WorkspaceError::engine)?,
WorkCounters::default(),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let merged = merge_generation_async(
&self.inner.objects,
MergeGenerationRequest {
base_generation: normalized_base_object,
base: normalized_base,
ours_generation: Some(target_object),
ours: target_root.clone(),
theirs_generation: normalized_source_object,
theirs: normalized_source,
retain_theirs_parent: history == crate::workspace::JoinHistory::Merge,
maximum_changes,
maximum_conflicts,
resolutions,
},
decode_limits(target.config),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
let (candidate, merged_root) = match merged.value {
MergeGenerationOutcome::Conflicted {
conflicts,
truncated,
} => return Ok(WorkspaceJoinOutcome::Conflicted(conflicts, truncated)),
MergeGenerationOutcome::Prepared {
generation_root,
root,
..
} => (generation_root, root),
};
if merged_root.file_table == target_root.file_table {
return Ok(WorkspaceJoinOutcome::NoChanges(current_target));
}
let publication = publish_generation_async_with_permit(
&self.inner.objects,
&self.inner.authority,
PublishGenerationRequest {
authority_id: volume_authority_id(target.id),
volume_id: target.id,
epoch: expected_head.epoch,
expected: expected_head,
operation_id,
generation_root: candidate,
},
PublicationIntent::guarded(Some(operation_context), permit),
closure_limits(target.config),
WorkBudget::UNBOUNDED,
&cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
Ok(match publication.outcome {
AppendOutcome::Committed(_) => {
WorkspaceJoinOutcome::Applied(publication.proof.generation_id)
}
AppendOutcome::AlreadyCommitted(_) => {
WorkspaceJoinOutcome::AlreadyApplied(publication.proof.generation_id)
}
AppendOutcome::Conflict { .. } => {
let (actual, _, _) = target
.resolve_head_generation(WorkBudget::UNBOUNDED, &cancellation)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
WorkspaceJoinOutcome::Stale(GenerationId::new(actual.digest))
}
AppendOutcome::Fenced { .. } => WorkspaceJoinOutcome::Fenced,
AppendOutcome::IdempotencyConflict { .. } => WorkspaceJoinOutcome::IdempotencyConflict,
})
}
pub async fn execute_residency(
&self,
speculator: &crate::ResidencySpeculator,
permit: crate::ResidencyPermit,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> crate::storage::ObjectResult<u64> {
crate::execute_residency(
speculator,
&self.inner.objects,
permit,
budget,
cancellation,
)
.await
}
pub async fn create_volume(
&self,
config: VolumeConfig,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Volume<A, O>> {
self.create_volume_with_id(VolumeId::new(), config, budget, cancellation)
.await
}
pub async fn create_volume_with_id(
&self,
volume_id: VolumeId,
config: VolumeConfig,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Volume<A, O>> {
self.create_volume_with_id_operation(volume_id, config, None, budget, cancellation)
.await
}
async fn create_volume_with_id_operation(
&self,
volume_id: VolumeId,
config: VolumeConfig,
operation_id: Option<OperationId>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Volume<A, O>> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
validate_volume_capabilities(config, self.inner.capabilities)?;
let (generation_root, mut work) = self
.build_empty_generation(volume_id, config, budget, cancellation)
.await?;
let closure_limits = closure_limits(config);
let remaining_budget = remaining(work, budget)?;
let proof = prove_generation_closure_async(
&self.inner.objects,
generation_root,
closure_limits,
remaining_budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, proof.work)?;
self.publish_volume_creation(
VolumeCreation {
volume_id,
config,
generation_root,
operation_id,
},
work,
budget,
cancellation,
)
.await
}
pub async fn open_volume(
&self,
volume_id: VolumeId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Volume<A, O>> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (creation, work) = self
.read_creation(volume_id, WorkCounters::default(), budget, cancellation)
.await?;
validate_volume_capabilities(creation.config, self.inner.capabilities)
.map_err(|failure| OperationFailure::new(failure.error, work))?;
Ok(FsReceipt {
value: Volume {
fs: self.clone(),
id: volume_id,
config: creation.config,
},
work,
})
}
pub async fn export_object(
&self,
object_id: ObjectId,
maximum_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<crate::storage::ObjectRead> {
let receipt = self
.inner
.objects
.read(object_id, maximum_bytes, budget, cancellation)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
Ok(FsReceipt {
value: receipt.value,
work: receipt.work,
})
}
pub async fn import_object(
&self,
object_id: ObjectId,
bytes: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let receipt = self
.inner
.objects
.put(object_id, bytes, budget, cancellation)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
Ok(FsReceipt {
value: (),
work: receipt.work,
})
}
pub async fn export_generation_batch(
&self,
manifest: &GenerationExportManifest,
cursor: TransferCursor,
maximum_objects: u32,
maximum_object_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<GenerationTransferBatch> {
export_generation_batch_async(
&self.inner.objects,
manifest,
cursor,
maximum_objects,
maximum_object_bytes,
budget,
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(map_transfer_error(failure.error), *failure.work))
}
pub async fn import_generation_batch(
&self,
manifest: &GenerationExportManifest,
cursor: TransferCursor,
bodies: &[Bytes],
maximum_objects: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<TransferCursor> {
import_generation_batch_async(
&self.inner.objects,
manifest,
cursor,
bodies,
maximum_objects,
budget,
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(map_transfer_error(failure.error), *failure.work))
}
pub async fn restore_volume(
&self,
manifest: &GenerationExportManifest,
operation_id: OperationId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Volume<A, O>> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
validate_volume_capabilities(manifest.config, self.inner.capabilities)?;
let proof = authenticate_generation_export_manifest_async(
&self.inner.objects,
manifest,
closure_limits(manifest.config),
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(map_transfer_error(failure.error), *failure.work)
})?;
let work = proof.work;
self.publish_volume_creation(
VolumeCreation {
volume_id: manifest.volume_id,
config: manifest.config,
generation_root: manifest.generation_root,
operation_id: Some(operation_id),
},
work,
budget,
cancellation,
)
.await
}
async fn publish_volume_creation(
&self,
creation: VolumeCreation,
mut work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Volume<A, O>> {
let VolumeCreation {
volume_id,
config,
generation_root,
operation_id,
} = creation;
let authority_id = volume_authority_id(volume_id);
let created = self
.inner
.authority
.create_authority(
authority_id,
Epoch::GENESIS,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, created.work)?;
let active_head = match created.value {
CreateAuthorityOutcome::Created(head) | CreateAuthorityOutcome::Existing(head) => head,
};
let event = encode_volume_created(VolumeCreated {
volume_id,
config,
initial_generation_root: generation_root,
})
.map_err(|error| OperationFailure::new(error.into(), work))?;
let operation_id = if let Some(operation_id) = operation_id {
operation_id
} else {
let (operation_id, identity_work) = derived_operation_id(volume_id);
work = add(work, identity_work)?;
operation_id
};
let (commit, encoding_work) = creation_commit(operation_id, event);
work = add(work, encoding_work)?;
work.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let drained = self
.inner
.objects
.flush_before_publish(remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, drained.work)?;
let appended = self
.inner
.authority
.compare_and_append(
authority_id,
active_head.epoch,
Head::genesis(active_head.epoch),
commit,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, appended.work)?;
match appended.value {
AppendOutcome::Committed(_) | AppendOutcome::AlreadyCommitted(_) => Ok(FsReceipt {
value: Volume {
fs: self.clone(),
id: volume_id,
config,
},
work,
}),
AppendOutcome::Conflict { .. }
| AppendOutcome::Fenced { .. }
| AppendOutcome::IdempotencyConflict { .. } => {
Err(OperationFailure::new(FsError::CreationRejected, work))
}
}
}
async fn read_creation(
&self,
volume_id: VolumeId,
work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(VolumeCreated, WorkCounters), OperationFailure<FsError>> {
let records = self
.inner
.authority
.replay(
volume_authority_id(volume_id),
Sequence::GENESIS,
ReplayLimit {
records: 1,
payload_bytes: MAXIMUM_VOLUME_EVENT_BYTES,
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
let combined = add(work, records.work)?;
let first = records
.value
.first()
.ok_or_else(|| OperationFailure::new(FsError::EmptyAuthority, combined))?;
let creation = decode_volume_created(&first.payload, MAXIMUM_VOLUME_EVENT_BYTES)
.map_err(|error| OperationFailure::new(error.into(), combined))?;
if creation.volume_id != volume_id {
return Err(OperationFailure::new(FsError::VolumeMismatch, combined));
}
Ok((creation, combined))
}
async fn build_empty_generation(
&self,
volume_id: VolumeId,
config: VolumeConfig,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(ObjectId, WorkCounters), OperationFailure<FsError>> {
let mut work = WorkCounters::default();
let metadata = self
.put_encoded(
ObjectKind::Metadata,
encode_file_metadata(empty_metadata())
.map_err(|error| OperationFailure::new(error.into(), work))?,
work,
budget,
cancellation,
)
.await?;
work = metadata.1;
let tree = self
.put_encoded(
ObjectKind::TreePage,
encode_tree_page(
&TreePage::Leaf(Vec::new()),
config.limits.maximum_directory_page_entries,
)
.map_err(|error| OperationFailure::new(error.into(), work))?,
work,
budget,
cancellation,
)
.await?;
work = tree.1;
let (root_file_id, identity_work) = derived_file_id(volume_id);
work = add(work, identity_work)?;
let records = FileTablePage::Leaf(vec![FileRecord {
file_id: root_file_id,
kind: FileKind::Directory,
link_count: 1,
metadata: metadata.0,
payload: FilePayload::Directory { entries: tree.0 },
}]);
work = add(
work,
WorkCounters {
allocation_operations: 1,
peak_allocation_bytes: u64::try_from(size_of::<FileRecord>()).unwrap_or(u64::MAX),
..WorkCounters::default()
},
)?;
let encoded_file_table =
encode_file_table_page(&records, config.limits.maximum_directory_page_entries)
.map_err(|error| OperationFailure::new(error.into(), work))?;
drop(records);
let file_table = self
.put_encoded(
ObjectKind::FileTablePage,
encoded_file_table,
work,
budget,
cancellation,
)
.await?;
work = file_table.1;
let root = GenerationRoot {
volume_id,
root_file_id,
file_table: file_table.0,
parents: Vec::new(),
required_features: 0,
};
self.put_encoded(
ObjectKind::GenerationRoot,
encode_generation_root(&root)
.map_err(|error| OperationFailure::new(error.into(), work))?,
work,
budget,
cancellation,
)
.await
}
async fn put_encoded(
&self,
kind: ObjectKind,
encoded: Vec<u8>,
work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(ObjectId, WorkCounters), OperationFailure<FsError>> {
let length = u64::try_from(encoded.len()).unwrap_or(u64::MAX);
let semantic = WorkCounters {
bytes_encoded: length,
bytes_hashed: length.saturating_add(OBJECT_DIGEST_ENVELOPE_BYTES),
allocation_operations: 1,
peak_allocation_bytes: u64::try_from(encoded.capacity()).unwrap_or(u64::MAX),
..WorkCounters::default()
};
let prospective = add(work, semantic)?;
prospective
.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let object = ObjectId {
kind,
digest: object_digest(kind, &encoded),
};
let live_bytes = semantic.peak_allocation_bytes;
let mut backend_budget = remaining(prospective, budget)?;
backend_budget.peak_allocation_bytes -= live_bytes;
let receipt = self
.inner
.objects
.put(object, Bytes::from(encoded), backend_budget, cancellation)
.await
.map_err(|failure| {
merge_simultaneous_failure(
prospective,
*failure.work,
live_bytes,
failure.error.into(),
)
})?;
Ok((
object,
merge_simultaneous_work(prospective, receipt.work, live_bytes, budget)?,
))
}
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Volume<A, O> {
#[must_use]
pub const fn id(&self) -> VolumeId {
self.id
}
#[must_use]
pub const fn config(&self) -> VolumeConfig {
self.config
}
pub async fn diff_generations(
&self,
before: GenerationId,
after: GenerationId,
maximum_changes: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<GenerationDiff> {
if maximum_changes == 0 {
return Err(OperationFailure::before_work(FsError::InvalidDiff));
}
if before == after {
return Ok(OperationReceipt {
value: GenerationDiff {
files: Vec::new(),
bindings: Vec::new(),
truncated: false,
},
work: WorkCounters::default(),
});
}
let mut work = WorkCounters::default();
let before_id = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: before.digest(),
};
let after_id = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: after.digest(),
};
let (before_root, before_work) = read_generation_root(
&self.fs.inner.objects,
before_id,
self.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, before_work)?;
let (after_root, after_work) = read_generation_root(
&self.fs.inner.objects,
after_id,
self.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, after_work)?;
if before_root.volume_id != self.id || after_root.volume_id != self.id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
diff_generation_file_tables(
&self.fs.inner.objects,
before_root.file_table,
after_root.file_table,
maximum_changes,
self.config,
work,
budget,
cancellation,
)
.await
}
async fn resolve_checkout_generation(
&self,
selector: GenerationSelector,
mode: CheckoutMode,
work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(ObjectId, Option<Head>, WorkCounters), OperationFailure<FsError>> {
match selector {
GenerationSelector::Exact(generation_id) => {
let mut work = work;
let authority_head = if mode.access == AccessMode::ReadWrite {
if mode.consistency != ConsistencyMode::TrackingSafe
|| mode.mutations != MutationMode::PrivateOverlay
{
return Err(OperationFailure::before_work(
FsError::WritableCheckoutRequiresHead,
));
}
let resolved = self
.resolve_head_generation(remaining(work, budget)?, cancellation)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, std::convert::identity)
})?;
work = add(work, resolved.2)?;
Some(resolved.1)
} else {
None
};
Ok((
ObjectId {
kind: ObjectKind::GenerationRoot,
digest: generation_id.digest(),
},
authority_head,
work,
))
}
GenerationSelector::Head => {
let resolved = self
.resolve_head_generation(remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
let work = add(work, resolved.2)?;
Ok((resolved.0, Some(resolved.1), work))
}
}
}
pub async fn checkout(
&self,
selector: GenerationSelector,
mode: CheckoutMode,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Checkout<A, O>> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
validate_checkout(mode, self.config)?;
let mut work = WorkCounters::default();
let (generation_root, mut authority_head, next_work) = self
.resolve_checkout_generation(selector, mode, work, budget, cancellation)
.await?;
work = next_work;
let (root, root_work) = read_generation_root(
&self.fs.inner.objects,
generation_root,
self.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, root_work)?;
if root.volume_id != self.id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
if mode.access == AccessMode::ReadWrite
&& self.config.concurrency == ConcurrencyMode::ExclusiveWriter
{
let expected = authority_head.ok_or_else(|| {
OperationFailure::new(FsError::WritableCheckoutRequiresHead, work)
})?;
let fenced = self
.fs
.inner
.authority
.fence(
volume_authority_id(self.id),
expected,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, fenced.work)?;
authority_head = Some(match fenced.value {
FenceOutcome::Advanced(head) => head,
FenceOutcome::Conflict { actual } => {
return Err(OperationFailure::new(
FsError::ExclusiveWriterConflict { actual },
work,
));
}
});
}
let dependencies = CheckoutDependencies::new(
std::iter::empty(),
std::iter::empty(),
self.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
Ok(FsReceipt {
value: Checkout {
volume: self.clone(),
base_generation_root: generation_root,
generation_root,
base_file_table: root.file_table,
base_root: root.clone(),
root,
authority_head,
authored_operation_id: None,
live_operation_id: None,
last_commit: None,
prepared_merge_parent: None,
dependencies,
mode,
},
work,
})
}
async fn resolve_head_generation(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(ObjectId, Head, WorkCounters), OperationFailure<FsError>> {
let authority_id = volume_authority_id(self.id);
let head = self
.fs
.inner
.authority
.head(authority_id, budget, cancellation)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
let mut work = head.work;
if head.value.sequence == Sequence::GENESIS {
return Err(OperationFailure::new(FsError::EmptyAuthority, work));
}
let after = Sequence::new(head.value.sequence.get().saturating_sub(1));
let records = self
.fs
.inner
.authority
.replay(
authority_id,
after,
ReplayLimit {
records: 1,
payload_bytes: MAXIMUM_VOLUME_EVENT_BYTES,
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, records.work)?;
let latest = records
.value
.first()
.ok_or_else(|| OperationFailure::new(FsError::InvalidAuthorityHistory, work))?;
if let Ok(deleted) = decode_workspace_deleted(&latest.payload, MAXIMUM_VOLUME_EVENT_BYTES) {
if deleted != self.id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
return Err(OperationFailure::new(FsError::WorkspaceDeleted, work));
}
let generation_root = generation_from_record(latest, self.id, work)?;
Ok((generation_root, head.value, work))
}
}
impl<A, O> Checkout<A, O> {
#[must_use]
pub const fn volume_id(&self) -> VolumeId {
self.volume.id
}
#[must_use]
pub const fn volume_config(&self) -> VolumeConfig {
self.volume.config
}
#[must_use]
pub const fn generation_id(&self) -> GenerationId {
GenerationId::new(self.generation_root.digest)
}
#[must_use]
pub const fn root(&self) -> &GenerationRoot {
&self.root
}
#[must_use]
pub const fn mode(&self) -> CheckoutMode {
self.mode
}
#[must_use]
pub fn has_pending_mutations(&self) -> bool {
self.root.file_table != self.base_file_table || self.prepared_merge_parent.is_some()
}
#[must_use]
pub fn snapshot_reader(&self) -> PinnedReader<A, O> {
PinnedReader {
volume: self.volume.clone(),
generation_root: self.generation_root,
root: self.root.clone(),
}
}
pub fn pinned_reader(&self) -> Result<PinnedReader<A, O>, FsError> {
if self.mode.consistency != ConsistencyMode::Pinned {
return Err(FsError::MutationNotAllowed);
}
Ok(self.snapshot_reader())
}
#[must_use]
pub fn content_stager(&self) -> ContentStager<A, O> {
ContentStager {
volume: self.volume.clone(),
}
}
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> ContentStager<A, O> {
pub async fn stage<R: AsyncBlobSource>(
&self,
source: &mut R,
maximum_source_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<StagedContent> {
stage_content_for_volume(
&self.volume,
source,
maximum_source_bytes,
budget,
cancellation,
)
.await
}
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> PinnedReader<A, O> {
async fn resolve_file_records(
&self,
paths: &[NamespacePath],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<Option<FileRecord>>> {
let lookup = crate::kernel::lookup_paths_async(
&self.volume.fs.inner.objects,
&self.root,
paths,
self.volume.config,
budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(WorkCounters::default(), FsError::Path))?;
let mut records = Vec::new();
records
.try_reserve_exact(lookup.entries.len())
.map_err(|_| {
OperationFailure::new(FsError::PendingMutationAllocationFailed, lookup.work)
})?;
for entry in lookup.entries {
records.push(entry.record);
}
Ok(FsReceipt {
value: records,
work: lookup.work,
})
}
async fn list_directory_page(
&self,
request: DirectoryPageRequest,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryPage> {
list_directory_page_pinned(
&self.volume,
&self.root,
&request.path,
request.after.as_ref(),
request.maximum_entries,
budget,
cancellation,
)
.await
}
pub async fn list_directory_pages(
&self,
requests: &[DirectoryPageRequest],
concurrency: usize,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<DirectoryPage>> {
if concurrency == 0
|| requests.len()
> usize::try_from(self.volume.config.limits.maximum_paths_per_batch)
.unwrap_or(usize::MAX)
{
return Err(OperationFailure::before_work(FsError::Path(
PathLookupError::TooManyPaths,
)));
}
if budget != WorkBudget::UNBOUNDED {
let mut work = WorkCounters::default();
let mut ordered = Vec::new();
ordered
.try_reserve_exact(requests.len())
.map_err(|_| OperationFailure::before_work(FsError::Work(WorkError::Overflow)))?;
for request in requests.iter().cloned() {
let receipt = self
.list_directory_page(request, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, receipt.work)?;
ordered.push(receipt.value);
}
return Ok(FsReceipt {
value: ordered,
work,
});
}
let results = stream::iter(
requests
.iter()
.cloned()
.enumerate()
.map(|(index, request)| {
let reader = self.clone();
async move {
(
index,
reader
.list_directory_page(request, budget, cancellation)
.await,
)
}
}),
)
.buffer_unordered(concurrency.min(requests.len().max(1)))
.collect::<Vec<_>>()
.await;
order_batch_results(results, budget)
}
pub async fn resolve_directory_page(
&self,
path: &NamespacePath,
after: Option<&LogicalName>,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<ResolvedDirectoryPage<A, O>> {
let listing = list_directory_page_pinned(
&self.volume,
&self.root,
path,
after,
maximum_entries,
budget,
cancellation,
)
.await?;
let mut work = listing.work;
let page = listing.value;
let mut file_ids = Vec::new();
file_ids
.try_reserve_exact(page.entries.len())
.map_err(|_| OperationFailure::new(FsError::Work(WorkError::Overflow), work))?;
file_ids.extend(page.entries.iter().map(|entry| entry.file_id));
let records = if file_ids.is_empty() {
Vec::new()
} else {
let receipt = lookup_file_records_async(
&self.volume.fs.inner.objects,
self.root.file_table,
&file_ids,
maximum_entries,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRecord))?;
work = add(work, receipt.work)?;
receipt.records
};
let mut metadata_requests = Vec::new();
metadata_requests
.try_reserve_exact(records.len())
.map_err(|_| OperationFailure::new(FsError::Work(WorkError::Overflow), work))?;
for record in &records {
let record = record
.as_ref()
.ok_or_else(|| OperationFailure::new(FsError::InvalidDirectoryRecord, work))?;
metadata_requests.push(ObjectReadRequest {
object_id: record.metadata,
maximum_bytes: self.volume.config.limits.maximum_object_bytes,
});
}
let metadata = if metadata_requests.is_empty() {
Vec::new()
} else {
let receipt = self
.volume
.fs
.inner
.objects
.read_many(&metadata_requests, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Object))?;
work = add(work, receipt.work)?;
receipt.value
};
let mut entries = Vec::new();
entries
.try_reserve_exact(page.entries.len())
.map_err(|_| OperationFailure::new(FsError::Work(WorkError::Overflow), work))?;
for ((binding, record), metadata) in page.entries.into_iter().zip(records).zip(metadata) {
let record = record
.ok_or_else(|| OperationFailure::new(FsError::InvalidDirectoryRecord, work))?;
if record.file_id != binding.file_id || record.kind != binding.kind {
return Err(OperationFailure::new(FsError::InvalidDirectoryRecord, work));
}
let metadata = decode_file_metadata(&metadata, decode_limits(self.volume.config))
.map_err(|error| OperationFailure::new(error.into(), work))?;
let logical_bytes = record_logical_bytes(record);
entries.push(ResolvedDirectoryEntry {
name: binding.name,
file: ResolvedFile {
reader: self.clone(),
record,
description: FileDescription {
kind: record.kind,
logical_bytes,
metadata,
},
},
});
}
Ok(FsReceipt {
value: ResolvedDirectoryPage {
entries,
has_more: page.has_more,
},
work,
})
}
async fn read_symbolic_link_record(
&self,
record: FileRecord,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Bytes> {
read_symbolic_link_record(&self.volume, record, budget, cancellation).await
}
async fn read_record_range(
&self,
record: FileRecord,
range: ByteRange,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
if range.length > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let read = read_file_range_async(
&self.volume.fs.inner.objects,
FileRangeRequest {
record,
range,
maximum_spans: self.volume.config.limits.maximum_directory_page_entries,
limits: decode_limits(self.volume.config),
budget,
},
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(WorkCounters::default(), FsError::FileRead)
})?;
Ok(FsReceipt {
work: read.work,
value: read,
})
}
async fn read_record_metadata(
&self,
record: FileRecord,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileMetadata> {
let metadata = self
.volume
.fs
.inner
.objects
.read(
record.metadata,
self.volume.config.limits.maximum_object_bytes,
budget,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(WorkCounters::default(), FsError::Object)
})?;
let value = decode_file_metadata(&metadata.value, decode_limits(self.volume.config))
.map_err(|error| OperationFailure::new(error.into(), metadata.work))?;
Ok(FsReceipt {
value,
work: metadata.work,
})
}
async fn resolve_file(
&self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRecord> {
let lookup = crate::kernel::observe_path_edges_async(
&self.volume.fs.inner.objects,
&self.root,
path,
self.volume.config,
budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(WorkCounters::default(), FsError::Path))?;
let work = lookup.lookup.work;
let record = lookup
.lookup
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
Ok(FsReceipt {
value: record,
work,
})
}
pub async fn plan_file_extents(
&self,
path: &NamespacePath,
range: ByteRange,
maximum_spans: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<ExtentPlan>> {
let resolved = self.resolve_file(path, budget, cancellation).await?;
let mut work = resolved.work;
let plan = self
.plan_record_extents(
resolved.value,
range,
maximum_spans,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, plan.work)?;
Ok(FsReceipt {
value: plan.value,
work,
})
}
async fn plan_record_extents(
&self,
record: FileRecord,
range: ByteRange,
maximum_spans: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<ExtentPlan>> {
if maximum_spans == 0 || range.length > self.volume.config.limits.maximum_generation_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let mut work = WorkCounters::default();
let (logical_bytes, extents) = match record.payload {
FilePayload::InlineRegular(data) => {
validate_planned_file_range(
u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX),
range,
work,
)?;
return Ok(FsReceipt { value: None, work });
}
FilePayload::Regular {
logical_bytes,
extents,
} => (logical_bytes, extents),
_ => {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
work,
));
}
};
let mut plan = plan_extent_range_async(
&self.volume.fs.inner.objects,
ExtentRangeRequest {
root: extents,
file_size: logical_bytes,
range,
maximum_spans,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, |error| {
FsError::FileRead(FileRangeReadError::Extent(error))
})
})?;
work = add(work, plan.work)?;
plan.work = work;
Ok(FsReceipt {
value: Some(plan),
work,
})
}
pub async fn read_file_range(
&self,
path: &NamespacePath,
range: ByteRange,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
if range.length > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let resolved = self.resolve_file(path, budget, cancellation).await?;
let mut work = resolved.work;
let record = resolved.value;
let read = read_file_range_async(
&self.volume.fs.inner.objects,
FileRangeRequest {
record,
range,
maximum_spans: self.volume.config.limits.maximum_directory_page_entries,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRead))?;
work = add(work, read.work)?;
Ok(FsReceipt { value: read, work })
}
pub async fn read_symbolic_link(
&self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Bytes> {
let resolved = self.resolve_file(path, budget, cancellation).await?;
let mut work = resolved.work;
let read = self
.read_symbolic_link_record(resolved.value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, read.work)?;
Ok(FsReceipt {
value: read.value,
work,
})
}
pub async fn read_metadata(
&self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileMetadata> {
let resolved = self.resolve_file(path, budget, cancellation).await?;
let mut work = resolved.work;
let metadata = self
.read_record_metadata(resolved.value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, metadata.work)?;
Ok(FsReceipt {
value: metadata.value,
work,
})
}
pub async fn describe_files(
&self,
paths: &[NamespacePath],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<Option<FileDescription>>> {
let resolved = self.resolve_files(paths, budget, cancellation).await?;
let descriptions = resolved
.value
.into_iter()
.map(|file| file.map(|file| file.description))
.collect();
Ok(FsReceipt {
value: descriptions,
work: resolved.work,
})
}
pub async fn resolve_files(
&self,
paths: &[NamespacePath],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<Option<ResolvedFile<A, O>>>> {
let resolved = self
.resolve_file_records(paths, budget, cancellation)
.await?;
let mut work = resolved.work;
let mut requests = Vec::new();
requests
.try_reserve_exact(resolved.value.len())
.map_err(|_| OperationFailure::new(FsError::Work(WorkError::Overflow), work))?;
requests.extend(
resolved
.value
.iter()
.flatten()
.map(|record| ObjectReadRequest {
object_id: record.metadata,
maximum_bytes: self.volume.config.limits.maximum_object_bytes,
}),
);
let reads = if requests.is_empty() {
Vec::new()
} else {
let receipt = self
.volume
.fs
.inner
.objects
.read_many(&requests, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Object))?;
work = add(work, receipt.work)?;
receipt.value
};
let mut reads = reads.into_iter();
let mut handles = Vec::new();
handles
.try_reserve_exact(resolved.value.len())
.map_err(|_| OperationFailure::new(FsError::Work(WorkError::Overflow), work))?;
for record in resolved.value {
let Some(record) = record else {
handles.push(None);
continue;
};
let bytes = reads
.next()
.ok_or_else(|| OperationFailure::new(FsError::Work(WorkError::Overflow), work))?;
let metadata = decode_file_metadata(&bytes, decode_limits(self.volume.config))
.map_err(|error| OperationFailure::new(error.into(), work))?;
let logical_bytes = record_logical_bytes(record);
handles.push(Some(ResolvedFile {
reader: self.clone(),
record,
description: FileDescription {
kind: record.kind,
logical_bytes,
metadata,
},
}));
}
Ok(FsReceipt {
value: handles,
work,
})
}
pub async fn read_resolved_ranges(
&self,
requests: &[ResolvedFileRangeReadRequest<'_, A, O>],
concurrency: usize,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<FileRangeRead>> {
if concurrency == 0
|| requests.len()
> usize::try_from(self.volume.config.limits.maximum_paths_per_batch)
.unwrap_or(usize::MAX)
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
if budget != WorkBudget::UNBOUNDED {
let mut work = WorkCounters::default();
let mut reads = Vec::new();
reads
.try_reserve_exact(requests.len())
.map_err(|_| OperationFailure::before_work(FsError::Work(WorkError::Overflow)))?;
for request in requests {
let read = request
.file
.read_range(request.range, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, read.work)?;
reads.push(read.value);
}
return Ok(FsReceipt { value: reads, work });
}
let cancellation = cancellation.clone();
let pending = requests
.iter()
.enumerate()
.map(|(index, request)| {
(
index,
request.file.reader.clone(),
request.file.record,
request.range,
)
})
.collect::<Vec<_>>();
let results = stream::iter(pending.into_iter().map(|(index, reader, record, range)| {
let cancellation = cancellation.clone();
async move {
(
index,
reader
.read_record_range(record, range, budget, &cancellation)
.await,
)
}
}))
.buffer_unordered(concurrency.min(requests.len().max(1)))
.collect::<Vec<_>>()
.await;
order_batch_results(results, budget)
}
}
fn record_logical_bytes(record: FileRecord) -> u64 {
match record.payload {
FilePayload::InlineRegular(bytes) => {
u64::try_from(bytes.as_bytes().len()).unwrap_or(u64::MAX)
}
FilePayload::Regular { logical_bytes, .. } => logical_bytes,
FilePayload::SymbolicLink { target_bytes, .. } => target_bytes,
_ => 0,
}
}
async fn list_directory_page_pinned<A: AsyncAuthorityStore, O: AsyncObjectStore>(
volume: &Volume<A, O>,
root: &GenerationRoot,
path: &NamespacePath,
after: Option<&LogicalName>,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryPage> {
let lookup = crate::kernel::observe_path_edges_async(
&volume.fs.inner.objects,
root,
path,
volume.config,
budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(WorkCounters::default(), FsError::Path))?;
let mut work = lookup.lookup.work;
let record = lookup
.lookup
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let FilePayload::Directory { entries } = record.payload else {
return Err(OperationFailure::new(FsError::NotDirectory, work));
};
if record.kind != FileKind::Directory {
return Err(OperationFailure::new(FsError::NotDirectory, work));
}
let mut page = list_tree_entries_async(
&volume.fs.inner.objects,
entries,
after,
maximum_entries,
decode_limits(volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Directory))?;
work = add(work, page.work)?;
page.work = work;
Ok(FsReceipt { value: page, work })
}
async fn read_symbolic_link_record<A: AsyncAuthorityStore, O: AsyncObjectStore>(
volume: &Volume<A, O>,
record: FileRecord,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Bytes> {
let FilePayload::SymbolicLink {
target_bytes,
target,
} = record.payload
else {
return Err(OperationFailure::before_work(FsError::NotSymbolicLink));
};
if target_bytes > volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
if target_bytes == 0 {
return Ok(FsReceipt {
value: Bytes::new(),
work: WorkCounters::default(),
});
}
let read = read_blob_range_async(
&volume.fs.inner.objects,
target,
ByteRange {
offset: 0,
length: target_bytes,
},
decode_limits(volume.config),
budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(WorkCounters::default(), FsError::BlobRead))?;
Ok(FsReceipt {
value: read.bytes,
work: read.work,
})
}
fn order_batch_results<T>(
mut results: Vec<(usize, FsResult<T>)>,
budget: WorkBudget,
) -> FsResult<Vec<T>> {
results.sort_unstable_by_key(|(index, _)| *index);
let mut work = WorkCounters::default();
let mut completed = Vec::new();
completed
.try_reserve_exact(results.len())
.map_err(|_| OperationFailure::before_work(FsError::Work(WorkError::Overflow)))?;
let mut first_failure = None;
for (index, result) in results {
match result {
Ok(receipt) => {
work = add(work, receipt.work)?;
completed.push((index, receipt.value));
}
Err(failure) => {
work = add(work, *failure.work)?;
if first_failure.is_none() {
first_failure = Some(failure.error);
}
}
}
}
work.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), work))?;
if let Some(error) = first_failure {
return Err(OperationFailure::new(error, work));
}
Ok(FsReceipt {
value: completed.into_iter().map(|(_, value)| value).collect(),
work,
})
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> Checkout<A, O> {
pub async fn prepare_merge(
&mut self,
theirs: GenerationId,
maximum_changes: u32,
maximum_conflicts: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<MergePreparation> {
if self.mode.access != AccessMode::ReadWrite
|| self.mode.mutations != MutationMode::PrivateOverlay
{
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
if self.prepared_merge_parent.is_some() {
return Err(OperationFailure::before_work(FsError::PreparedMergePending));
}
if maximum_changes == 0 || maximum_conflicts == 0 {
return Err(OperationFailure::before_work(FsError::MergeChangeLimit));
}
let theirs_id = ObjectId {
kind: ObjectKind::GenerationRoot,
digest: theirs.digest(),
};
let (current_head_root, current_head, mut work) = self
.volume
.resolve_head_generation(budget, cancellation)
.await?;
if current_head_root != theirs_id {
return Err(OperationFailure::new(FsError::MergeParentNotHead, work));
}
let (theirs_root, theirs_work) = read_generation_root(
&self.volume.fs.inner.objects,
theirs_id,
self.volume.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, theirs_work)?;
let merged = merge_generation_async(
&self.volume.fs.inner.objects,
MergeGenerationRequest {
base_generation: self.base_generation_root,
base: self.base_root.clone(),
ours_generation: (self.root.file_table == self.base_file_table)
.then_some(self.generation_root),
ours: self.root.clone(),
theirs_generation: theirs_id,
theirs: theirs_root,
retain_theirs_parent: true,
maximum_changes,
maximum_conflicts,
resolutions: BTreeMap::new(),
},
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| map_merge_failure(failure, work))?;
work = add(work, merged.work)?;
match merged.value {
MergeGenerationOutcome::Prepared {
generation_root,
root,
generation_id,
} => {
self.root = root;
self.generation_root = generation_root;
self.prepared_merge_parent = Some(theirs_id);
self.authority_head = Some(current_head);
Ok(OperationReceipt {
value: MergePreparation::Prepared { generation_id },
work,
})
}
MergeGenerationOutcome::Conflicted {
conflicts,
truncated,
} => Ok(OperationReceipt {
value: MergePreparation::Conflicted {
conflicts,
truncated,
},
work,
}),
}
}
pub async fn export_manifest(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<GenerationExportManifest> {
let checkpoint = self.checkpoint_root(budget, cancellation).await?;
let generation_root = checkpoint.value;
let mut work = checkpoint.work;
let manifest = build_generation_export_manifest_async(
&self.volume.fs.inner.objects,
self.volume.id,
self.volume.config,
generation_root,
closure_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, map_transfer_error))?;
work = add(work, manifest.work)?;
Ok(FsReceipt {
value: manifest.value,
work,
})
}
pub async fn checkpoint(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<GenerationId> {
let checkpoint = self.checkpoint_root(budget, cancellation).await?;
Ok(FsReceipt {
value: GenerationId::new(checkpoint.value.digest),
work: checkpoint.work,
})
}
async fn checkpoint_root(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<ObjectId> {
if self.prepared_merge_parent.is_some() || !self.has_pending_mutations() {
return Ok(FsReceipt {
value: self.generation_root,
work: WorkCounters::default(),
});
}
let checkpoint = build_checkpoint_async(
&self.volume.fs.inner.objects,
CheckpointRequest {
base: self.base_generation_root,
file_table: self.root.file_table,
merge_parent: None,
},
decode_limits(self.volume.config),
budget,
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
Ok(FsReceipt {
value: checkpoint.root,
work: checkpoint.work,
})
}
pub async fn discard(
&mut self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if self.mode.mutations == MutationMode::None {
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
let (root, work) = read_generation_root(
&self.volume.fs.inner.objects,
self.base_generation_root,
self.volume.config,
budget,
cancellation,
)
.await?;
if root.volume_id != self.volume.id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
self.root = root;
self.base_root = self.root.clone();
self.base_file_table = self.root.file_table;
self.generation_root = self.base_generation_root;
self.live_operation_id = None;
self.prepared_merge_parent = None;
self.dependencies.clear();
Ok(FsReceipt { value: (), work })
}
pub async fn refresh_head(
&mut self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<GenerationId> {
if !matches!(
self.mode.consistency,
ConsistencyMode::Manual | ConsistencyMode::TrackingSafe
) {
return Err(OperationFailure::before_work(FsError::RefreshNotAllowed));
}
if self.has_pending_mutations() {
return Err(OperationFailure::before_work(
FsError::PendingMutationsRequireRebase,
));
}
let (generation_root, head, mut work) = self
.volume
.resolve_head_generation(budget, cancellation)
.await?;
if generation_root == self.generation_root {
self.authority_head = Some(head);
return Ok(FsReceipt {
value: self.generation_id(),
work,
});
}
let (root, root_work) = read_generation_root(
&self.volume.fs.inner.objects,
generation_root,
self.volume.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, root_work)?;
if root.volume_id != self.volume.id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
self.base_generation_root = generation_root;
self.generation_root = generation_root;
self.root = root;
self.base_root = self.root.clone();
self.base_file_table = self.root.file_table;
self.authority_head = Some(head);
self.dependencies.clear();
Ok(FsReceipt {
value: self.generation_id(),
work,
})
}
pub async fn rebase_head(
&mut self,
maximum_conflicts: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<RebaseDecision> {
if self.live_operation_id.is_some() {
return Err(OperationFailure::before_work(FsError::PendingLiveMutation));
}
self.rebase_head_for_live_retry(maximum_conflicts, budget, cancellation)
.await
}
async fn rebase_head_for_live_retry(
&mut self,
maximum_conflicts: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<RebaseDecision> {
if self.prepared_merge_parent.is_some() {
return Err(OperationFailure::before_work(FsError::PreparedMergePending));
}
if !matches!(
self.mode.consistency,
ConsistencyMode::TrackingSafe | ConsistencyMode::Manual | ConsistencyMode::Live
) {
return Err(OperationFailure::before_work(FsError::RefreshNotAllowed));
}
let (candidate_object, candidate_head, mut work) = self
.volume
.resolve_head_generation(budget, cancellation)
.await?;
let base = GenerationId::new(self.base_generation_root.digest);
let candidate = GenerationId::new(candidate_object.digest);
let probe = AuthenticatedGenerationProbe::new(
&self.volume.fs.inner.objects,
ProbeLimits {
decode: decode_limits(self.volume.config),
maximum_cached_generations: 2,
maximum_cached_records: self.volume.config.limits.maximum_checkout_dependencies,
maximum_extent_spans: self.volume.config.limits.maximum_directory_page_entries,
maximum_content_payload_bytes: self.volume.config.limits.maximum_read_bytes,
maximum_directory_entries: self.volume.config.limits.maximum_directory_page_entries,
},
)
.map_err(|error| OperationFailure::new(FsError::Rebase(RebaseError::Probe(error)), work))?;
let classification = classify_rebase_async(
&probe,
base,
candidate,
&self.dependencies,
maximum_conflicts,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Rebase))?;
work = add(work, classification.work)?;
if matches!(classification.decision, RebaseDecision::Conflicted { .. }) {
return Ok(FsReceipt {
value: classification.decision,
work,
});
}
if candidate_object == self.base_generation_root {
self.authority_head = Some(candidate_head);
return Ok(FsReceipt {
value: classification.decision,
work,
});
}
let (candidate_root, root_work) = read_generation_root(
&self.volume.fs.inner.objects,
candidate_object,
self.volume.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, root_work)?;
if candidate_root.volume_id != self.volume.id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
let candidate_file_table = candidate_root.file_table;
let candidate_base_root = candidate_root.clone();
let rebased_root = self
.rebase_private_candidate(
candidate_object,
&candidate_root,
maximum_conflicts,
&mut work,
budget,
cancellation,
)
.await?;
self.base_generation_root = candidate_object;
self.base_file_table = candidate_file_table;
self.base_root = candidate_base_root;
self.generation_root = candidate_object;
self.root = rebased_root;
self.authority_head = Some(candidate_head);
Ok(FsReceipt {
value: classification.decision,
work,
})
}
async fn rebase_private_candidate(
&self,
candidate_object: ObjectId,
candidate_root: &GenerationRoot,
maximum_conflicts: u32,
work: &mut WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<GenerationRoot, OperationFailure<FsError>> {
if self.root.file_table == self.base_file_table {
return Ok(candidate_root.clone());
}
let checkpoint = self
.checkpoint_root(remaining(*work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(*work, std::convert::identity))?;
*work = add(*work, checkpoint.work)?;
let merged = merge_generation_async(
&self.volume.fs.inner.objects,
MergeGenerationRequest {
base_generation: self.base_generation_root,
base: self.base_root.clone(),
ours_generation: Some(candidate_object),
ours: candidate_root.clone(),
theirs_generation: checkpoint.value,
theirs: self.root.clone(),
retain_theirs_parent: false,
maximum_changes: u32::MAX,
maximum_conflicts,
resolutions: BTreeMap::new(),
},
decode_limits(self.volume.config),
remaining(*work, budget)?,
cancellation,
)
.await
.map_err(|failure| map_merge_failure(failure, *work))?;
*work = add(*work, merged.work)?;
match merged.value {
MergeGenerationOutcome::Prepared { root, .. } => Ok(root),
MergeGenerationOutcome::Conflicted { .. } => {
Err(OperationFailure::new(FsError::InvalidDiff, *work))
}
}
}
pub async fn refresh_live(
&mut self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<GenerationId> {
if self.mode.consistency != ConsistencyMode::Live {
return Err(OperationFailure::before_work(FsError::RefreshNotAllowed));
}
let synchronized = self.synchronize_live(budget, cancellation).await?;
Ok(FsReceipt {
value: self.generation_id(),
work: synchronized.work,
})
}
async fn synchronize_live(
&mut self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if self.mode.consistency != ConsistencyMode::Live {
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
}
let rebase = self
.rebase_head(
self.volume.config.limits.maximum_checkout_dependencies,
budget,
cancellation,
)
.await?;
match rebase.value {
RebaseDecision::Safe { .. } => Ok(FsReceipt {
value: (),
work: rebase.work,
}),
RebaseDecision::Conflicted {
conflicts,
truncated,
} => Err(OperationFailure::new(
FsError::LiveConflict {
conflicts,
truncated,
},
rebase.work,
)),
}
}
async fn capture_mutation_dependencies(
&self,
operations: &[Mutation],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<Dependency>> {
let mut work = WorkCounters::default();
let mut dependencies = Vec::new();
let mut exact_regions = Vec::new();
exact_regions
.try_reserve_exact(operations.len().saturating_mul(4))
.map_err(|_| OperationFailure::before_work(FsError::PendingMutationAllocationFailed))?;
for operation in operations {
let Some([first_path, second_path]) = operation.paths() else {
let records = self
.identity_base_records(operation, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, records.work)?;
let (first_record, second_record) = records.value;
for known_region in exact_mutation_regions(operation, first_record, second_record)
.into_iter()
.flatten()
{
exact_regions.push(known_region);
}
continue;
};
let first = crate::kernel::observe_path_edges_async(
&self.volume.fs.inner.objects,
&self.base_root,
first_path,
self.volume.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Path))?;
work = add(work, first.lookup.work)?;
dependencies.extend(first.dependencies);
let second_record = if second_path == first_path {
first.lookup.record
} else {
let second = crate::kernel::observe_path_edges_async(
&self.volume.fs.inner.objects,
&self.base_root,
second_path,
self.volume.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Path))?;
work = add(work, second.lookup.work)?;
dependencies.extend(second.dependencies);
second.lookup.record
};
for known_region in
exact_mutation_regions(operation, first.lookup.record, second_record)
.into_iter()
.flatten()
{
exact_regions.push(known_region);
}
}
let captured = capture_known_dependencies_async(
&self.volume.fs.inner.objects,
self.volume.config,
exact_regions,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, captured.work)?;
dependencies.extend(captured.value);
Ok(FsReceipt {
value: dependencies,
work,
})
}
async fn identity_base_records(
&self,
operation: &Mutation,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<(Option<FileRecord>, Option<FileRecord>)> {
let (first_id, second_id) = match operation {
Mutation::File { file_id, .. } => (Some(*file_id), None),
Mutation::CloneFileRange {
source_file_id,
destination_file_id,
..
} => (Some(*source_file_id), Some(*destination_file_id)),
_ => (None, None),
};
let mut work = WorkCounters::default();
let first_record = if let Some(file_id) = first_id {
let lookup = lookup_file_record_async(
&self.volume.fs.inner.objects,
self.base_root.file_table,
file_id,
decode_limits(self.volume.config),
budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRecord))?;
work = add(work, lookup.work)?;
lookup.record
} else {
None
};
let second_record = if second_id == first_id {
first_record
} else if let Some(file_id) = second_id {
let lookup = lookup_file_record_async(
&self.volume.fs.inner.objects,
self.base_root.file_table,
file_id,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRecord))?;
work = add(work, lookup.work)?;
lookup.record
} else {
None
};
Ok(FsReceipt {
value: (first_record, second_record),
work,
})
}
pub async fn mutate(
&mut self,
operations: Vec<Mutation>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if self.mode.access != AccessMode::ReadWrite
|| !matches!(
self.mode.mutations,
MutationMode::PrivateOverlay | MutationMode::DirectLive
)
{
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
self.mutate_candidate(operations, budget, cancellation)
.await
}
pub async fn mutate_live(
&mut self,
operations: Vec<Mutation>,
operation_id: OperationId,
maximum_attempts: u32,
maximum_conflicts: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<LiveMutationOutcome> {
self.validate_live_operation(maximum_attempts)?;
if self.has_pending_mutations() {
return Err(OperationFailure::before_work(FsError::PendingLiveMutation));
}
let mutation = self
.mutate_candidate(operations, budget, cancellation)
.await?;
let mut work = mutation.work;
let publication = self
.resume_live(
operation_id,
maximum_attempts,
maximum_conflicts,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, publication.work)?;
Ok(FsReceipt {
value: publication.value,
work,
})
}
pub async fn apply_authored_live(
&mut self,
authored: Vec<AuthoredMutation>,
operation_id: OperationId,
maximum_attempts: u32,
maximum_conflicts: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<AuthoredLiveMutationResult> {
self.validate_live_operation(maximum_attempts)?;
if self.has_pending_mutations() {
return Err(OperationFailure::before_work(FsError::PendingLiveMutation));
}
let transaction = self
.apply_authored_transaction(authored, budget, cancellation)
.await?;
let mut work = transaction.work;
let created_file_ids = transaction.value.created_file_ids;
let publication = self
.resume_live(
operation_id,
maximum_attempts,
maximum_conflicts,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, publication.work)?;
Ok(FsReceipt {
value: AuthoredLiveMutationResult {
created_file_ids,
outcome: publication.value,
},
work,
})
}
pub async fn resume_live(
&mut self,
operation_id: OperationId,
maximum_attempts: u32,
maximum_conflicts: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<LiveMutationOutcome> {
self.validate_live_operation(maximum_attempts)?;
if !self.has_pending_mutations() {
return Err(OperationFailure::before_work(FsError::NoPendingMutations));
}
match self.live_operation_id {
Some(retained) if retained != operation_id => {
return Err(OperationFailure::before_work(FsError::PendingLiveMutation));
}
Some(_) => {}
None => self.live_operation_id = Some(operation_id),
}
let publication = self
.publish_live(
operation_id,
maximum_attempts,
maximum_conflicts,
budget,
cancellation,
)
.await?;
if matches!(
publication.value,
LiveMutationOutcome::Committed { .. }
| LiveMutationOutcome::AlreadyCommitted { .. }
| LiveMutationOutcome::IdempotencyConflict { .. }
) {
self.live_operation_id = None;
}
Ok(publication)
}
fn validate_live_operation(
&self,
maximum_attempts: u32,
) -> Result<(), OperationFailure<FsError>> {
if self.mode.access != AccessMode::ReadWrite
|| self.mode.consistency != ConsistencyMode::Live
|| self.mode.mutations != MutationMode::DirectLive
{
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
if maximum_attempts == 0 {
return Err(OperationFailure::before_work(FsError::ZeroLiveRetryLimit));
}
Ok(())
}
async fn publish_live(
&mut self,
operation_id: OperationId,
maximum_attempts: u32,
maximum_conflicts: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<LiveMutationOutcome> {
let mut work = WorkCounters::default();
let mut state = LiveRetryState::new(maximum_attempts)
.map_err(|error| OperationFailure::before_work(error.into()))?;
loop {
let publication = self
.publish_pending(operation_id, None, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, publication.work)?;
let action = state
.observe_publication(live_publication_observation(publication.value))
.map_err(|error| OperationFailure::new(error.into(), work))?;
match action {
LiveRetryAction::Complete(outcome) => {
return Ok(FsReceipt {
value: outcome,
work,
});
}
LiveRetryAction::Publish => {
return Err(OperationFailure::new(
FsError::LiveRetry(LiveRetryError::InvalidTransition),
work,
));
}
LiveRetryAction::Rebase => {
let rebase = self
.rebase_head_for_live_retry(
maximum_conflicts,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, std::convert::identity)
})?;
work = add(work, rebase.work)?;
match state
.observe_rebase(rebase.value)
.map_err(|error| OperationFailure::new(error.into(), work))?
{
LiveRetryAction::Publish => {}
LiveRetryAction::Complete(outcome) => {
return Ok(FsReceipt {
value: outcome,
work,
});
}
LiveRetryAction::Rebase => {
return Err(OperationFailure::new(
FsError::LiveRetry(LiveRetryError::InvalidTransition),
work,
));
}
}
}
}
}
}
async fn mutate_candidate(
&mut self,
mut operations: Vec<Mutation>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if self.live_operation_id.is_some() {
return Err(OperationFailure::before_work(FsError::PendingLiveMutation));
}
if self.prepared_merge_parent.is_some() {
return Err(OperationFailure::before_work(FsError::PreparedMergePending));
}
let maximum = usize::try_from(self.volume.config.limits.maximum_mutations_per_batch)
.unwrap_or(usize::MAX);
if operations.capacity() > maximum {
return Err(OperationFailure::before_work(
FsError::ExcessiveMutationCapacity,
));
}
let admitted =
Box::pin(self.admit_mutation_names(&mut operations, budget, cancellation)).await?;
let captured =
Box::pin(self.capture_mutation_dependencies(&operations, budget, cancellation)).await?;
let dependency_work = add(admitted.work, captured.work)?;
let mutation_dependencies = captured.value;
let mut next_dependencies = self.dependencies.clone();
next_dependencies
.extend_mutations(
mutation_dependencies,
self.volume.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), dependency_work))?;
let prior_file_table = self.root.file_table;
let (receipt, _) = Box::pin(apply_generation_mutations_retaining_async(
&self.volume.fs.inner.objects,
&self.root,
operations,
self.volume.config,
remaining(dependency_work, budget)?,
cancellation,
))
.await
.map_err(|failure| failure.map_with_prior_work(dependency_work, FsError::Mutation))?;
let work = add(dependency_work, receipt.work)?;
if receipt.root.file_table != prior_file_table {
if receipt.root.file_table == self.base_file_table {
next_dependencies.clear_mutations();
}
self.dependencies = next_dependencies;
self.root = receipt.root;
}
Ok(FsReceipt { value: (), work })
}
#[allow(clippy::too_many_lines)]
async fn admit_mutation_names(
&mut self,
operations: &mut [Mutation],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if self.volume.config.case_sensitivity != CaseSensitivity::ProfileFolded
&& self.volume.config.unicode != UnicodePolicy::RequireNfc
{
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
}
let mut work = WorkCounters::default();
let mut pending: Vec<(NamespacePath, FileId)> = Vec::new();
let mut removed: Vec<(NamespacePath, FileId)> = Vec::new();
pending
.try_reserve_exact(operations.len())
.map_err(|_| OperationFailure::before_work(FsError::PendingMutationAllocationFailed))?;
removed
.try_reserve_exact(operations.len())
.map_err(|_| OperationFailure::before_work(FsError::PendingMutationAllocationFailed))?;
for operation in operations.iter_mut() {
cancellation
.check()
.map_err(|error| OperationFailure::new(error.into(), work))?;
let canonicalized = Box::pin(self.canonicalize_existing_mutation(
operation,
&pending,
remaining(work, budget)?,
cancellation,
))
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, canonicalized.work)?;
let parent_canonicalized = Box::pin(self.canonicalize_new_mutation_parents(
operation,
&pending,
remaining(work, budget)?,
cancellation,
))
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, parent_canonicalized.work)?;
match operation {
Mutation::Create { path, record } | Mutation::Restore { path, record } => {
let allowed = removed_binding_id(path, &removed);
let allowed_ids: &[FileId] = match allowed {
Some(ref file_id) => std::slice::from_ref(file_id),
None => &[],
};
let parent_pending = pending_parent_exists(
path,
&pending,
self.volume.config.case_sensitivity,
self.volume.config.limits,
);
let admission = Box::pin(self.admit_new_name(
path,
remaining(work, budget)?,
cancellation,
allowed_ids,
parent_pending,
))
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, admission.work)?;
if self.volume.config.case_sensitivity == CaseSensitivity::ProfileFolded
&& pending_name_collision(path, &pending, allowed_ids)
{
return Err(OperationFailure::new(FsError::NameCollision, work));
}
pending.push((path.clone(), record.file_id));
}
Mutation::Link {
source,
destination,
} => {
let source_id = self
.pending_or_existing_file_id(
source,
&pending,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, std::convert::identity)
})?;
work = add(work, source_id.work)?;
let allowed = removed_binding_id(destination, &removed);
let allowed_ids: &[FileId] = match allowed {
Some(ref file_id) => std::slice::from_ref(file_id),
None => &[],
};
let parent_pending = pending_parent_exists(
destination,
&pending,
self.volume.config.case_sensitivity,
self.volume.config.limits,
);
let admission = Box::pin(self.admit_new_name(
destination,
remaining(work, budget)?,
cancellation,
allowed_ids,
parent_pending,
))
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, admission.work)?;
if self.volume.config.case_sensitivity == CaseSensitivity::ProfileFolded
&& pending_name_collision(destination, &pending, allowed_ids)
{
return Err(OperationFailure::new(FsError::NameCollision, work));
}
pending.push((destination.clone(), source_id.value));
}
Mutation::Rename {
source,
destination,
replace,
} => {
let source_id = self
.pending_or_existing_file_id(
source,
&pending,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, std::convert::identity)
})?;
work = add(work, source_id.work)?;
let mut allowed_ids = Vec::with_capacity(2);
allowed_ids.push(source_id.value);
if let Some(file_id) = removed_binding_id(destination, &removed)
&& file_id != source_id.value
{
allowed_ids.push(file_id);
}
let parent_pending = pending_parent_exists(
destination,
&pending,
self.volume.config.case_sensitivity,
self.volume.config.limits,
);
let admission = Box::pin(self.admit_new_name(
destination,
remaining(work, budget)?,
cancellation,
&allowed_ids,
parent_pending,
))
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, admission.work)?;
if self.volume.config.case_sensitivity == CaseSensitivity::ProfileFolded
&& pending_name_collision(destination, &pending, &allowed_ids)
{
return Err(OperationFailure::new(FsError::NameCollision, work));
}
pending.retain(|(path, _)| path != source);
if *replace {
pending.retain(|(path, _)| path != destination);
}
removed.push((source.clone(), source_id.value));
pending.push((destination.clone(), source_id.value));
}
Mutation::Remove { path, .. } => {
if pending_binding_id(path, &pending, self.volume.config.case_sensitivity)
.is_none()
{
let removed_id = self
.pending_or_existing_file_id(
path,
&pending,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, std::convert::identity)
})?;
work = add(work, removed_id.work)?;
removed.push((path.clone(), removed_id.value));
}
pending.retain(|(pending_path, _)| pending_path != path);
}
Mutation::SetMetadata { .. }
| Mutation::Write { .. }
| Mutation::ValidateRegular { .. }
| Mutation::Resize { .. }
| Mutation::ZeroRange { .. }
| Mutation::Preallocate { .. }
| Mutation::CloneRange { .. }
| Mutation::File { .. }
| Mutation::CloneFileRange { .. } => {}
}
}
Ok(FsReceipt { value: (), work })
}
async fn canonicalize_new_mutation_parents(
&mut self,
operation: &mut Mutation,
pending: &[(NamespacePath, FileId)],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if self.volume.config.case_sensitivity != CaseSensitivity::ProfileFolded {
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
}
let (Mutation::Create { path, .. }
| Mutation::Link {
destination: path, ..
}
| Mutation::Rename {
destination: path, ..
}) = operation
else {
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
};
let Some((parent, name)) = path.split_last() else {
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
};
if parent.is_empty() {
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
}
let resolved = self
.canonicalize_path_components(parent, pending, budget, cancellation)
.await?;
let work = resolved.work;
let Some(resolved_parent) = resolved.value else {
return Ok(FsReceipt { value: (), work });
};
let mut components = resolved_parent.components().to_vec();
components.push(name.clone());
*path = NamespacePath::new(components, self.volume.config.limits).map_err(|error| {
OperationFailure::new(FsError::Path(PathLookupError::NamespacePath(error)), work)
})?;
Ok(FsReceipt { value: (), work })
}
async fn canonicalize_existing_mutation(
&mut self,
operation: &mut Mutation,
pending: &[(NamespacePath, FileId)],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if self.volume.config.case_sensitivity != CaseSensitivity::ProfileFolded {
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
}
let mut work = WorkCounters::default();
let paths = match operation {
Mutation::Remove { path, .. }
| Mutation::SetMetadata { path, .. }
| Mutation::Write { path, .. }
| Mutation::ValidateRegular { path }
| Mutation::Resize { path, .. }
| Mutation::ZeroRange { path, .. }
| Mutation::Preallocate { path, .. } => {
let resolved = self
.canonical_existing_path(path, pending, remaining(work, budget)?, cancellation)
.await?;
*path = resolved.value;
work = add(work, resolved.work)?;
return Ok(FsReceipt { value: (), work });
}
Mutation::Rename { source, .. } | Mutation::Link { source, .. } => {
let resolved = self
.canonical_existing_path(
source,
pending,
remaining(work, budget)?,
cancellation,
)
.await?;
*source = resolved.value;
work = add(work, resolved.work)?;
return Ok(FsReceipt { value: (), work });
}
Mutation::CloneRange {
source,
destination,
..
} => Some((source, destination)),
Mutation::Create { .. }
| Mutation::Restore { .. }
| Mutation::File { .. }
| Mutation::CloneFileRange { .. } => None,
};
if let Some((source, destination)) = paths {
let resolved_source = self
.canonical_existing_path(source, pending, remaining(work, budget)?, cancellation)
.await?;
*source = resolved_source.value;
work = add(work, resolved_source.work)?;
let resolved_destination = self
.canonical_existing_path(
destination,
pending,
remaining(work, budget)?,
cancellation,
)
.await?;
*destination = resolved_destination.value;
work = add(work, resolved_destination.work)?;
}
Ok(FsReceipt { value: (), work })
}
async fn canonical_existing_path(
&mut self,
path: &NamespacePath,
pending: &[(NamespacePath, FileId)],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<NamespacePath> {
if let Some((exact, _)) =
pending_binding_path(path, pending, self.volume.config.case_sensitivity)
{
return Ok(FsReceipt {
value: exact,
work: WorkCounters::default(),
});
}
let mut work = WorkCounters::default();
let exact = self
.lookup_no_follow_at_current(path, budget, cancellation, false)
.await?;
work = add(work, exact.work)?;
if exact.value.record.is_some()
|| self.volume.config.case_sensitivity != CaseSensitivity::ProfileFolded
{
if exact.value.record.is_some() {
return Ok(FsReceipt {
value: path.clone(),
work,
});
}
return Err(OperationFailure::new(FsError::NotFound, work));
}
if path.is_root() {
return Err(OperationFailure::new(FsError::NotFound, work));
}
let resolved = self
.canonicalize_path_components(
path.components(),
pending,
remaining(work, budget)?,
cancellation,
)
.await?;
work = add(work, resolved.work)?;
let canonical = resolved
.value
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
Ok(FsReceipt {
value: canonical,
work,
})
}
async fn canonicalize_path_components(
&mut self,
requested_components: &[LogicalName],
pending: &[(NamespacePath, FileId)],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<NamespacePath>> {
let mut work = WorkCounters::default();
let mut canonical_components = Vec::new();
for requested_component in requested_components {
let candidate = NamespacePath::new(
canonical_components
.iter()
.cloned()
.chain(std::iter::once(requested_component.clone()))
.collect(),
self.volume.config.limits,
)
.map_err(|error| {
OperationFailure::new(FsError::Path(PathLookupError::NamespacePath(error)), work)
})?;
if let Some((pending_path, _)) =
pending_binding_path(&candidate, pending, self.volume.config.case_sensitivity)
{
canonical_components.push(
pending_path
.components()
.last()
.cloned()
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?,
);
continue;
}
let probe = self
.lookup_no_follow_at_current(
&candidate,
remaining(work, budget)?,
cancellation,
false,
)
.await?;
work = add(work, probe.work)?;
if probe.value.record.is_some() {
canonical_components.push(requested_component.clone());
continue;
}
let parent =
NamespacePath::new(canonical_components.clone(), self.volume.config.limits)
.map_err(|error| {
OperationFailure::new(
FsError::Path(PathLookupError::NamespacePath(error)),
work,
)
})?;
let siblings = self
.find_case_folded_sibling(
parent.components(),
requested_component,
remaining(work, budget)?,
cancellation,
&[],
)
.await?;
work = add(work, siblings.work)?;
let Some(exact_name) = siblings.value else {
return Ok(FsReceipt { value: None, work });
};
canonical_components.push(exact_name);
}
let canonical = NamespacePath::new(canonical_components, self.volume.config.limits)
.map_err(|error| {
OperationFailure::new(FsError::Path(PathLookupError::NamespacePath(error)), work)
})?;
Ok(FsReceipt {
value: Some(canonical),
work,
})
}
async fn pending_or_existing_file_id(
&mut self,
path: &NamespacePath,
pending: &[(NamespacePath, FileId)],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
if let Some(file_id) =
pending_binding_id(path, pending, self.volume.config.case_sensitivity)
{
return Ok(FsReceipt {
value: file_id,
work: WorkCounters::default(),
});
}
let lookup = self.lookup_no_follow(path, budget, cancellation).await?;
let work = lookup.work;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
Ok(FsReceipt {
value: record.file_id,
work,
})
}
pub async fn apply_authored_transaction(
&mut self,
authored: Vec<AuthoredMutation>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<AuthoredTransactionResult> {
let maximum = usize::try_from(self.volume.config.limits.maximum_mutations_per_batch)
.unwrap_or(usize::MAX);
let operation_count = authored
.iter()
.try_fold(0_usize, |count, mutation| {
count.checked_add(authored_mutation_operation_count(mutation))
})
.ok_or_else(|| OperationFailure::before_work(FsError::TooManyPendingMutations))?;
if authored.len() > maximum || authored.capacity() > maximum || operation_count > maximum {
return Err(OperationFailure::before_work(
FsError::TooManyPendingMutations,
));
}
let mut work = WorkCounters::default();
let mut operations = Vec::new();
let mut created_file_ids = Vec::new();
let mut staged_metadata = AuthoredMetadataCache::new();
operations
.try_reserve_exact(operation_count)
.map_err(|_| OperationFailure::before_work(FsError::PendingMutationAllocationFailed))?;
created_file_ids
.try_reserve_exact(authored.len())
.map_err(|_| OperationFailure::before_work(FsError::PendingMutationAllocationFailed))?;
for authored_mutation in authored {
cancellation
.check()
.map_err(|error| OperationFailure::new(error.into(), work))?;
let created = self
.compile_authored_mutation(
authored_mutation,
&mut operations,
&mut work,
&mut staged_metadata,
budget,
cancellation,
)
.await?;
created_file_ids.push(created);
}
if operations.is_empty() {
return Ok(FsReceipt {
value: AuthoredTransactionResult { created_file_ids },
work,
});
}
let mutation = self
.mutate(operations, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt {
value: AuthoredTransactionResult { created_file_ids },
work,
})
}
pub(crate) async fn apply_authored_bulk_transaction(
&mut self,
authored: Vec<AuthoredMutation>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let maximum = usize::try_from(self.volume.config.limits.maximum_mutations_per_batch)
.unwrap_or(usize::MAX);
if maximum == 0 {
return Err(OperationFailure::before_work(
FsError::TooManyPendingMutations,
));
}
let mut candidate = self.private_candidate();
let mut work = WorkCounters::default();
let mut batch = Vec::with_capacity(maximum.min(authored.len()));
let mut operations = 0;
for mutation in authored {
let count = authored_mutation_operation_count(&mutation);
if count > maximum {
return Err(OperationFailure::new(
FsError::TooManyPendingMutations,
work,
));
}
if !batch.is_empty() && (batch.len() == maximum || operations + count > maximum) {
let applied = candidate
.apply_authored_transaction(batch, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, applied.work)?;
batch = Vec::with_capacity(maximum.min(1024));
operations = 0;
}
operations += count;
batch.push(mutation);
}
if !batch.is_empty() {
let applied = candidate
.apply_authored_transaction(batch, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, applied.work)?;
}
*self = candidate;
Ok(FsReceipt { value: (), work })
}
pub(crate) fn private_candidate(&self) -> Self {
Self {
volume: self.volume.clone(),
base_generation_root: self.base_generation_root,
generation_root: self.generation_root,
base_file_table: self.base_file_table,
base_root: self.base_root.clone(),
root: self.root.clone(),
authority_head: self.authority_head,
authored_operation_id: self.authored_operation_id,
live_operation_id: self.live_operation_id,
last_commit: self.last_commit,
prepared_merge_parent: self.prepared_merge_parent,
dependencies: self.dependencies.clone(),
mode: self.mode,
}
}
#[allow(clippy::too_many_lines)]
async fn compile_authored_mutation(
&self,
authored: AuthoredMutation,
operations: &mut Vec<Mutation>,
work: &mut WorkCounters,
staged_metadata: &mut AuthoredMetadataCache,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<Option<FileId>, OperationFailure<FsError>> {
match authored {
AuthoredMutation::Reidentify { path, file_id } => {
let current = crate::kernel::lookup_path_async(
&self.volume.fs.inner.objects,
&self.root,
&path,
self.volume.config,
remaining(*work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(*work, FsError::Path))?;
*work = add(*work, current.work)?;
let current = current
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, *work))?;
if current.kind == FileKind::Directory {
return Err(OperationFailure::new(FsError::InvalidSpecialKind, *work));
}
if current.file_id == file_id {
return Ok(None);
}
let existing = self
.lookup_file_record_by_id(file_id, remaining(*work, budget)?, cancellation)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, existing.work)?;
if existing.value.is_some_and(|existing| {
existing.kind != current.kind
|| existing.metadata != current.metadata
|| existing.payload != current.payload
}) {
return Err(OperationFailure::new(FsError::FileIdentityCollision, *work));
}
operations.push(Mutation::Restore {
path,
record: FileRecord {
file_id,
link_count: 1,
..current
},
});
Ok(None)
}
AuthoredMutation::CreateFile {
path,
bytes,
metadata,
} => {
if u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> self.volume.config.limits.maximum_read_bytes
{
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::InvalidRange),
*work,
));
}
let metadata = self
.stage_authored_metadata(
metadata,
staged_metadata,
remaining(*work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, metadata.work)?;
let file_id = self.authored_file_id(FileKind::Regular, &path);
if bytes.len() <= crate::kernel::MAXIMUM_INLINE_FILE_BYTES {
operations.push(Mutation::Create {
path,
record: FileRecord {
file_id,
kind: FileKind::Regular,
link_count: 1,
metadata: metadata.value,
payload: FilePayload::InlineRegular(
InlineFileData::new(&bytes)
.map_err(|error| OperationFailure::new(error.into(), *work))?,
),
},
});
} else {
let blob = self
.stage_blob(bytes, remaining(*work, budget)?, cancellation)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, blob.work)?;
operations.push(Mutation::Create {
path: path.clone(),
record: FileRecord {
file_id,
kind: FileKind::Regular,
link_count: 1,
metadata: metadata.value,
payload: FilePayload::InlineRegular(
InlineFileData::new(&[])
.map_err(|error| OperationFailure::new(error.into(), *work))?,
),
},
});
operations.push(Mutation::Write {
path,
offset: 0,
length: blob.value.logical_bytes,
content: blob.value.root,
content_offset: 0,
});
}
Ok(Some(file_id))
}
AuthoredMutation::CreateFileFromContent {
path,
content,
metadata,
file_id,
} => {
let metadata = self
.stage_authored_metadata(
metadata,
staged_metadata,
remaining(*work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, metadata.work)?;
let file_id =
file_id.unwrap_or_else(|| self.authored_file_id(FileKind::Regular, &path));
operations.push(Mutation::Create {
path: path.clone(),
record: FileRecord {
file_id,
kind: FileKind::Regular,
link_count: 1,
metadata: metadata.value,
payload: FilePayload::InlineRegular(
InlineFileData::new(&[])
.map_err(|error| OperationFailure::new(error.into(), *work))?,
),
},
});
if content.logical_bytes != 0 {
operations.push(Mutation::Write {
path,
offset: 0,
length: content.logical_bytes,
content: content.root,
content_offset: 0,
});
}
Ok(Some(file_id))
}
AuthoredMutation::CreateDirectory { path, metadata } => {
let tree = self
.stage_empty_tree(remaining(*work, budget)?, cancellation)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, tree.work)?;
self.compile_record(
operations,
path,
FileKind::Directory,
FilePayload::Directory {
entries: tree.value,
},
metadata,
work,
staged_metadata,
budget,
cancellation,
)
.await
.map(Some)
}
AuthoredMutation::CreateSymbolicLink {
path,
target,
metadata,
} => {
let blob = self
.stage_blob(target, remaining(*work, budget)?, cancellation)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, blob.work)?;
self.compile_record(
operations,
path,
FileKind::SymbolicLink,
FilePayload::SymbolicLink {
target_bytes: blob.value.logical_bytes,
target: blob.value.root,
},
metadata,
work,
staged_metadata,
budget,
cancellation,
)
.await
.map(Some)
}
AuthoredMutation::CreateEmptySpecial {
path,
kind,
metadata,
} => {
if !matches!(
kind,
FileKind::Fifo | FileKind::Socket | FileKind::MountBoundary
) {
return Err(OperationFailure::new(FsError::InvalidSpecialKind, *work));
}
self.validate_profile_kind(kind, *work)?;
self.compile_record(
operations,
path,
kind,
FilePayload::Empty,
metadata,
work,
staged_metadata,
budget,
cancellation,
)
.await
.map(Some)
}
AuthoredMutation::CreateDevice {
path,
kind,
major,
minor,
metadata,
} => {
if !matches!(kind, FileKind::CharacterDevice | FileKind::BlockDevice) {
return Err(OperationFailure::new(FsError::InvalidSpecialKind, *work));
}
self.validate_profile_kind(kind, *work)?;
self.compile_record(
operations,
path,
kind,
FilePayload::Device { major, minor },
metadata,
work,
staged_metadata,
budget,
cancellation,
)
.await
.map(Some)
}
AuthoredMutation::CreateReparsePoint {
path,
payload,
metadata,
} => {
self.validate_profile_kind(FileKind::ReparsePoint, *work)?;
let blob = self
.stage_blob(payload, remaining(*work, budget)?, cancellation)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, blob.work)?;
self.compile_record(
operations,
path,
FileKind::ReparsePoint,
FilePayload::ReparsePoint {
payload_bytes: blob.value.logical_bytes,
payload: blob.value.root,
},
metadata,
work,
staged_metadata,
budget,
cancellation,
)
.await
.map(Some)
}
AuthoredMutation::Remove {
path,
expected_file_id,
} => {
operations.push(Mutation::Remove {
path,
expected_file_id: expected_file_id
.map_or(MetadataField::Unavailable, MetadataField::Value),
});
Ok(None)
}
AuthoredMutation::Rename {
source,
destination,
replace,
} => {
operations.push(Mutation::Rename {
source,
destination,
replace,
});
Ok(None)
}
AuthoredMutation::HardLink {
source,
destination,
} => {
operations.push(Mutation::Link {
source,
destination,
});
Ok(None)
}
AuthoredMutation::Write {
path,
offset,
bytes,
} => {
if bytes.is_empty() {
operations.push(Mutation::ValidateRegular { path });
} else {
let blob = self
.stage_blob(bytes, remaining(*work, budget)?, cancellation)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, blob.work)?;
operations.push(Mutation::Write {
path,
offset,
length: blob.value.logical_bytes,
content: blob.value.root,
content_offset: 0,
});
}
Ok(None)
}
AuthoredMutation::WriteFromContent {
path,
offset,
content,
} => {
if content.logical_bytes == 0 {
operations.push(Mutation::ValidateRegular { path });
} else {
operations.push(Mutation::Write {
path,
offset,
length: content.logical_bytes,
content: content.root,
content_offset: 0,
});
}
Ok(None)
}
AuthoredMutation::SetMetadata { path, metadata } => {
let metadata = self
.stage_authored_metadata(
metadata,
staged_metadata,
remaining(*work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(*work, std::convert::identity)
})?;
*work = add(*work, metadata.work)?;
operations.push(Mutation::SetMetadata {
path,
metadata: metadata.value,
});
Ok(None)
}
AuthoredMutation::Resize {
path,
logical_bytes,
} => {
operations.push(Mutation::Resize {
path,
logical_bytes,
});
Ok(None)
}
AuthoredMutation::ZeroRange {
path,
range,
allocated,
extend,
} => {
operations.push(Mutation::ZeroRange {
path,
offset: range.offset,
length: range.length,
allocated,
extend,
});
Ok(None)
}
AuthoredMutation::Preallocate {
path,
range,
keep_size,
} => {
operations.push(Mutation::Preallocate {
path,
offset: range.offset,
length: range.length,
keep_size,
});
Ok(None)
}
AuthoredMutation::CloneRange(request) => {
operations.push(Mutation::CloneRange {
source: request.source,
source_offset: request.source_offset,
destination: request.destination,
destination_offset: request.destination_offset,
length: request.length,
});
Ok(None)
}
}
}
fn validate_profile_kind(
&self,
kind: FileKind,
work: WorkCounters,
) -> Result<(), OperationFailure<FsError>> {
if kind.is_supported_by_profile(self.volume.config.profile) {
Ok(())
} else {
Err(OperationFailure::new(FsError::UnsupportedFileKind, work))
}
}
#[allow(clippy::too_many_arguments)]
async fn compile_record(
&self,
operations: &mut Vec<Mutation>,
path: NamespacePath,
kind: FileKind,
payload: FilePayload,
metadata: FileMetadata,
work: &mut WorkCounters,
staged_metadata: &mut AuthoredMetadataCache,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<FileId, OperationFailure<FsError>> {
let staged = self
.stage_authored_metadata(
metadata,
staged_metadata,
remaining(*work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(*work, std::convert::identity))?;
*work = add(*work, staged.work)?;
let file_id = self.authored_file_id(kind, &path);
operations.push(Mutation::Create {
path,
record: FileRecord {
file_id,
kind,
link_count: 1,
metadata: staged.value,
payload,
},
});
Ok(file_id)
}
pub(crate) fn bind_authored_operation(&mut self, operation_id: OperationId) {
self.authored_operation_id = Some(operation_id);
}
fn authored_file_id(&self, kind: FileKind, path: &NamespacePath) -> FileId {
let Some(operation_id) = self.authored_operation_id else {
return FileId::new();
};
let mut hasher = blake3::Hasher::new();
hasher.update(b"acyclic-fs-authored-file-id-v1\0");
hasher.update(&operation_id.into_bytes());
hasher.update(&[kind.tag()]);
for component in path.components() {
hasher.update(&[component.encoding().tag()]);
hasher.update(
&u64::try_from(component.as_bytes().len())
.unwrap_or(u64::MAX)
.to_le_bytes(),
);
hasher.update(component.as_bytes());
}
let digest = hasher.finalize();
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest.as_bytes()[..16]);
FileId::from_bytes(bytes)
}
async fn stage_metadata(
&self,
metadata: FileMetadata,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<ObjectId> {
let encoded = encode_file_metadata(metadata)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (value, work) = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encoded,
WorkCounters::default(),
budget,
cancellation,
)
.await?;
Ok(FsReceipt { value, work })
}
async fn stage_authored_metadata(
&self,
metadata: FileMetadata,
staged: &mut AuthoredMetadataCache,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<ObjectId> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
if let Some(value) = staged.get(metadata) {
return Ok(FsReceipt {
value,
work: WorkCounters::default(),
});
}
let receipt = self.stage_metadata(metadata, budget, cancellation).await?;
staged.remember(metadata, receipt.value);
Ok(receipt)
}
async fn stage_empty_tree(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<ObjectId> {
let encoded = encode_tree_page(
&TreePage::Leaf(Vec::new()),
self.volume.config.limits.maximum_directory_page_entries,
)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (value, work) = self
.volume
.fs
.put_encoded(
ObjectKind::TreePage,
encoded,
WorkCounters::default(),
budget,
cancellation,
)
.await?;
Ok(FsReceipt { value, work })
}
async fn stage_empty_attributes(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<ObjectId> {
let encoded = encode_attribute_page(
&AttributePage::Leaf(Vec::new()),
self.volume.config.limits.maximum_directory_page_entries,
)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (value, work) = self
.volume
.fs
.put_encoded(
ObjectKind::AttributePage,
encoded,
WorkCounters::default(),
budget,
cancellation,
)
.await?;
Ok(FsReceipt { value, work })
}
async fn admit_new_name(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
allowed_file_ids: &[FileId],
parent_pending: bool,
) -> FsResult<()> {
let mut work = WorkCounters::default();
let Some((parent_components, name)) = path.split_last() else {
return Ok(FsReceipt { value: (), work });
};
if self.volume.config.unicode == UnicodePolicy::RequireNfc
&& name.encoding() == NameEncoding::Utf8
{
let is_nfc =
std::str::from_utf8(name.as_bytes()).is_ok_and(unicode_normalization::is_nfc);
if !is_nfc {
return Err(OperationFailure::new(FsError::NonNormalizedName, work));
}
}
if self.volume.config.case_sensitivity == CaseSensitivity::ProfileFolded && !parent_pending
{
let found = self
.find_case_folded_sibling(
parent_components,
name,
remaining(work, budget)?,
cancellation,
allowed_file_ids,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, found.work)?;
if found.value.is_some() {
return Err(OperationFailure::new(FsError::NameCollision, work));
}
}
Ok(FsReceipt { value: (), work })
}
async fn find_case_folded_sibling(
&mut self,
parent_components: &[LogicalName],
target: &LogicalName,
budget: WorkBudget,
cancellation: &CancellationToken,
allowed_file_ids: &[FileId],
) -> FsResult<Option<LogicalName>> {
let mut work = WorkCounters::default();
let Ok(parent) = NamespacePath::new(parent_components.to_vec(), self.volume.config.limits)
else {
return Ok(FsReceipt { value: None, work });
};
let folded_target = target.case_fold_key();
let mut after: Option<LogicalName> = None;
loop {
let page = Box::pin(self.list_directory(
&parent,
after.as_ref(),
CASE_FOLD_COLLISION_SCAN_PAGE,
remaining(work, budget)?,
cancellation,
))
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, page.work)?;
for entry in &page.value.entries {
if entry.name != *target
&& entry.name.case_fold_key() == folded_target
&& !allowed_file_ids.contains(&entry.file_id)
{
return Ok(FsReceipt {
value: Some(entry.name.clone()),
work,
});
}
}
if !page.value.has_more {
break;
}
after = page.value.entries.last().map(|entry| entry.name.clone());
}
Ok(FsReceipt { value: None, work })
}
pub async fn create_file(
&mut self,
path: NamespacePath,
bytes: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
if u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> self.volume.config.limits.maximum_read_bytes
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let mut work = WorkCounters::default();
let metadata = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encode_file_metadata(empty_metadata())
.map_err(|error| OperationFailure::new(error.into(), work))?,
work,
budget,
cancellation,
)
.await?;
work = metadata.1;
let file_id = FileId::new();
let mut operations = Vec::new();
if bytes.len() <= crate::kernel::MAXIMUM_INLINE_FILE_BYTES {
operations.push(Mutation::Create {
path,
record: FileRecord {
file_id,
kind: FileKind::Regular,
link_count: 1,
metadata: metadata.0,
payload: FilePayload::InlineRegular(
InlineFileData::new(&bytes)
.map_err(|error| OperationFailure::new(error.into(), work))?,
),
},
});
} else {
let blob = self
.stage_blob(bytes, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, blob.work)?;
let length = blob.value.logical_bytes;
operations.push(Mutation::Create {
path: path.clone(),
record: FileRecord {
file_id,
kind: FileKind::Regular,
link_count: 1,
metadata: metadata.0,
payload: FilePayload::InlineRegular(
InlineFileData::new(&[])
.map_err(|error| OperationFailure::new(error.into(), work))?,
),
},
});
operations.push(Mutation::Write {
path,
offset: 0,
length,
content: blob.value.root,
content_offset: 0,
});
}
let mutation = self
.mutate(operations, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt {
value: file_id,
work,
})
}
pub async fn create_directory(
&mut self,
path: NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
let mut work = WorkCounters::default();
let metadata = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encode_file_metadata(empty_metadata())
.map_err(|error| OperationFailure::new(error.into(), work))?,
work,
budget,
cancellation,
)
.await?;
work = metadata.1;
let tree = self
.volume
.fs
.put_encoded(
ObjectKind::TreePage,
encode_tree_page(
&TreePage::Leaf(Vec::new()),
self.volume.config.limits.maximum_directory_page_entries,
)
.map_err(|error| OperationFailure::new(error.into(), work))?,
work,
budget,
cancellation,
)
.await?;
work = tree.1;
let file_id = FileId::new();
let mutation = self
.mutate(
vec![Mutation::Create {
path,
record: FileRecord {
file_id,
kind: FileKind::Directory,
link_count: 1,
metadata: metadata.0,
payload: FilePayload::Directory { entries: tree.0 },
},
}],
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt {
value: file_id,
work,
})
}
pub async fn create_symbolic_link(
&mut self,
path: NamespacePath,
target: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
let blob = self.stage_blob(target, budget, cancellation).await?;
let mut work = blob.work;
let created = self
.create_record(
path,
FileKind::SymbolicLink,
FilePayload::SymbolicLink {
target_bytes: blob.value.logical_bytes,
target: blob.value.root,
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, created.work)?;
Ok(FsReceipt {
value: created.value,
work,
})
}
pub async fn read_symbolic_link(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Bytes> {
let lookup = self.lookup_no_follow(path, budget, cancellation).await?;
let mut work = lookup.work;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let read =
read_symbolic_link_record(&self.volume, record, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, read.work)?;
Ok(FsReceipt {
value: read.value,
work,
})
}
pub async fn create_empty_special(
&mut self,
path: NamespacePath,
kind: FileKind,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
if !matches!(
kind,
FileKind::Fifo | FileKind::Socket | FileKind::MountBoundary
) {
return Err(OperationFailure::before_work(FsError::InvalidSpecialKind));
}
self.validate_profile_kind(kind, WorkCounters::default())?;
self.create_record(path, kind, FilePayload::Empty, budget, cancellation)
.await
}
pub async fn create_device(
&mut self,
path: NamespacePath,
kind: FileKind,
major: u32,
minor: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
if !matches!(kind, FileKind::CharacterDevice | FileKind::BlockDevice) {
return Err(OperationFailure::before_work(FsError::InvalidSpecialKind));
}
self.validate_profile_kind(kind, WorkCounters::default())?;
self.create_record(
path,
kind,
FilePayload::Device { major, minor },
budget,
cancellation,
)
.await
}
pub async fn create_reparse_point(
&mut self,
path: NamespacePath,
payload: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
self.validate_profile_kind(FileKind::ReparsePoint, WorkCounters::default())?;
let blob = self.stage_blob(payload, budget, cancellation).await?;
let mut work = blob.work;
let created = self
.create_record(
path,
FileKind::ReparsePoint,
FilePayload::ReparsePoint {
payload_bytes: blob.value.logical_bytes,
payload: blob.value.root,
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, created.work)?;
Ok(FsReceipt {
value: created.value,
work,
})
}
pub async fn read_reparse_point(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Bytes> {
let lookup = self.lookup_no_follow(path, budget, cancellation).await?;
let mut work = lookup.work;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let FilePayload::ReparsePoint {
payload_bytes,
payload,
} = record.payload
else {
return Err(OperationFailure::new(FsError::NotReparsePoint, work));
};
if payload_bytes > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::InvalidRange),
work,
));
}
if payload_bytes == 0 {
return Ok(FsReceipt {
value: Bytes::new(),
work,
});
}
let read = read_blob_range_async(
&self.volume.fs.inner.objects,
payload,
ByteRange {
offset: 0,
length: payload_bytes,
},
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::BlobRead))?;
work = add(work, read.work)?;
Ok(FsReceipt {
value: read.bytes,
work,
})
}
async fn create_record(
&mut self,
path: NamespacePath,
kind: FileKind,
payload: FilePayload,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileId> {
let mut work = WorkCounters::default();
let metadata = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encode_file_metadata(empty_metadata())
.map_err(|error| OperationFailure::new(error.into(), work))?,
work,
budget,
cancellation,
)
.await?;
work = metadata.1;
let file_id = FileId::new();
let mutation = self
.mutate(
vec![Mutation::Create {
path,
record: FileRecord {
file_id,
kind,
link_count: 1,
metadata: metadata.0,
payload,
},
}],
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt {
value: file_id,
work,
})
}
pub async fn write_file(
&mut self,
path: NamespacePath,
offset: u64,
bytes: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if bytes.is_empty() {
return self
.mutate(
vec![Mutation::ValidateRegular { path }],
budget,
cancellation,
)
.await;
}
if u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> self.volume.config.limits.maximum_read_bytes
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let blob = self.stage_blob(bytes, budget, cancellation).await?;
let mut work = blob.work;
let mutation = self
.mutate(
vec![Mutation::Write {
path,
offset,
length: blob.value.logical_bytes,
content: blob.value.root,
content_offset: 0,
}],
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn detach_regular_file(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DetachedFile<A, O>> {
let lookup = self
.lookup_no_follow_observed(path, budget, cancellation, true)
.await?;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, lookup.work))?;
if record.kind != FileKind::Regular {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
lookup.work,
));
}
Ok(FsReceipt {
value: DetachedFile {
volume: self.volume.clone(),
record,
},
work: lookup.work,
})
}
#[cfg(feature = "native-mount")]
pub(crate) fn detached_from_record(&self, record: FileRecord) -> DetachedFile<A, O> {
DetachedFile::from_record(self.volume.clone(), record)
}
pub async fn read_file_record_by_id(
&mut self,
file_id: FileId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRecord> {
let lookup = self
.lookup_file_record_by_id(file_id, budget, cancellation)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
if self.mode.consistency != ConsistencyMode::Pinned {
work = self
.observe_identity_region(
DependencyRegion::FileRecord(file_id),
work,
budget,
cancellation,
)
.await?;
}
Ok(FsReceipt {
value: record,
work,
})
}
pub async fn read_file_range_by_id(
&mut self,
file_id: FileId,
range: ByteRange,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
self.read_file_by_id_range(file_id, range, false, budget, cancellation)
.await
}
pub async fn read_file_up_to_by_id(
&mut self,
file_id: FileId,
offset: u64,
maximum_bytes: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
self.read_file_by_id_range(
file_id,
ByteRange {
offset,
length: u64::from(maximum_bytes),
},
true,
budget,
cancellation,
)
.await
}
async fn read_file_by_id_range(
&mut self,
file_id: FileId,
mut range: ByteRange,
clip_to_eof: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
let requested_range = range;
if range.length > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let lookup = self
.lookup_file_record_by_id(file_id, budget, cancellation)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
if clip_to_eof {
if record.kind != FileKind::Regular {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
work,
));
}
let logical_bytes = match record.payload {
FilePayload::InlineRegular(data) => {
u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX)
}
FilePayload::Regular { logical_bytes, .. } => logical_bytes,
_ => {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
work,
));
}
};
range.length = range.length.min(logical_bytes.saturating_sub(range.offset));
if range.length == 0 {
if self.mode.consistency != ConsistencyMode::Pinned && requested_range.length != 0 {
work = self
.observe_base_regular_range(
file_id,
requested_range,
work,
budget,
cancellation,
)
.await?;
}
return Ok(FsReceipt {
value: FileRangeRead {
bytes: Bytes::new(),
work,
},
work,
});
}
}
let mut read = read_file_range_async(
&self.volume.fs.inner.objects,
FileRangeRequest {
record,
range,
maximum_spans: self.volume.config.limits.maximum_directory_page_entries,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRead))?;
work = add(work, read.work)?;
if self.mode.consistency != ConsistencyMode::Pinned && requested_range.length != 0 {
work = self
.observe_base_regular_range(file_id, requested_range, work, budget, cancellation)
.await?;
}
read.work = work;
Ok(FsReceipt { value: read, work })
}
pub async fn plan_file_extents_by_id(
&mut self,
file_id: FileId,
range: ByteRange,
maximum_spans: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<ExtentPlan>> {
if maximum_spans == 0 || range.length > self.volume.config.limits.maximum_generation_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let lookup = self
.lookup_file_record_by_id(file_id, budget, cancellation)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let (logical_bytes, extents) = match record.payload {
FilePayload::InlineRegular(data) => {
let logical_bytes = u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX);
validate_planned_file_range(logical_bytes, range, work)?;
if self.mode.consistency != ConsistencyMode::Pinned && range.length != 0 {
work = self
.observe_base_regular_range(file_id, range, work, budget, cancellation)
.await?;
}
return Ok(FsReceipt { value: None, work });
}
FilePayload::Regular {
logical_bytes,
extents,
} => (logical_bytes, extents),
FilePayload::Directory { .. }
| FilePayload::SymbolicLink { .. }
| FilePayload::Device { .. }
| FilePayload::Empty
| FilePayload::ReparsePoint { .. } => {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
work,
));
}
};
let mut plan = plan_extent_range_async(
&self.volume.fs.inner.objects,
ExtentRangeRequest {
root: extents,
file_size: logical_bytes,
range,
maximum_spans,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, |error| {
FsError::FileRead(FileRangeReadError::Extent(error))
})
})?;
work = add(work, plan.work)?;
if self.mode.consistency != ConsistencyMode::Pinned && range.length != 0 {
work = self
.observe_base_regular_range(file_id, range, work, budget, cancellation)
.await?;
}
plan.work = work;
Ok(FsReceipt {
value: Some(plan),
work,
})
}
pub async fn seek_file_extent_by_id(
&mut self,
file_id: FileId,
offset: u64,
target: ExtentSeekTarget,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<u64>> {
let lookup = self
.lookup_file_record_by_id(file_id, budget, cancellation)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let value = match record.payload {
FilePayload::InlineRegular(data) => {
let logical_bytes = u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX);
if offset > logical_bytes {
None
} else {
match target {
ExtentSeekTarget::Data => (offset < logical_bytes).then_some(offset),
ExtentSeekTarget::Hole => Some(logical_bytes),
}
}
}
FilePayload::Regular {
logical_bytes,
extents,
} => {
if offset > logical_bytes {
None
} else {
let receipt = seek_extent_async(
&self.volume.fs.inner.objects,
ExtentSeekRequest {
root: extents,
file_size: logical_bytes,
offset,
target,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, |error| {
FsError::FileRead(FileRangeReadError::Extent(error))
})
})?;
work = add(work, receipt.work)?;
receipt.value
}
}
FilePayload::Directory { .. }
| FilePayload::SymbolicLink { .. }
| FilePayload::Empty
| FilePayload::Device { .. }
| FilePayload::ReparsePoint { .. } => {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
work,
));
}
};
if self.mode.consistency != ConsistencyMode::Pinned {
work = self
.observe_identity_region(
DependencyRegion::SparseSeek {
file_id,
offset,
target,
},
work,
budget,
cancellation,
)
.await?;
}
Ok(FsReceipt { value, work })
}
pub async fn read_metadata_by_id(
&mut self,
file_id: FileId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileMetadata> {
let lookup = self
.lookup_file_record_by_id(file_id, budget, cancellation)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let metadata = self
.volume
.fs
.inner
.objects
.read(
record.metadata,
self.volume.config.limits.maximum_object_bytes,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Object))?;
work = add(work, metadata.work)?;
let value = decode_file_metadata(&metadata.value, decode_limits(self.volume.config))
.map_err(|error| OperationFailure::new(error.into(), work))?;
if self.mode.consistency != ConsistencyMode::Pinned {
work = self
.observe_base_metadata_ids(
std::slice::from_ref(&file_id),
work,
budget,
cancellation,
)
.await?;
}
Ok(FsReceipt { value, work })
}
pub async fn read_named_attribute_by_id(
&mut self,
file_id: FileId,
name: &AttributeName,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<Bytes>> {
let metadata = self
.read_metadata_by_id(file_id, budget, cancellation)
.await?;
let mut work = metadata.work;
let MetadataField::Value(root) = metadata.value.named_attributes else {
return Ok(FsReceipt { value: None, work });
};
let lookup = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, lookup.work)?;
let Some(entry) = lookup.entry else {
return Ok(FsReceipt { value: None, work });
};
if entry.value_bytes > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::InvalidRange),
work,
));
}
let read = read_blob_range_async(
&self.volume.fs.inner.objects,
entry.value,
ByteRange {
offset: 0,
length: entry.value_bytes,
},
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::BlobRead))?;
work = add(work, read.work)?;
Ok(FsReceipt {
value: Some(read.bytes),
work,
})
}
pub async fn list_named_attributes_by_id(
&mut self,
file_id: FileId,
after: Option<&AttributeName>,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<AttributeListing> {
let metadata = self
.read_metadata_by_id(file_id, budget, cancellation)
.await?;
let mut work = metadata.work;
let MetadataField::Value(root) = metadata.value.named_attributes else {
let value = AttributeListing {
entries: Vec::new(),
has_more: false,
next_residency: None,
work,
};
return Ok(FsReceipt { value, work });
};
let mut listing = list_attributes_async(
&self.volume.fs.inner.objects,
root,
after,
maximum_entries,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeList))?;
work = add(work, listing.work)?;
listing.work = work;
Ok(FsReceipt {
value: listing,
work,
})
}
pub async fn write_named_attribute_by_id(
&mut self,
file_id: FileId,
name: AttributeName,
value: Bytes,
mode: NamedAttributeWriteMode,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let metadata = self
.read_metadata_by_id(file_id, budget, cancellation)
.await?;
let mut work = metadata.work;
let mut metadata_value = metadata.value;
let root = match metadata_value.named_attributes {
MetadataField::Unavailable => {
let empty = self
.stage_empty_attributes(remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, empty.work)?;
empty.value
}
MetadataField::Value(root) => root,
};
let current = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
&name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, current.work)?;
match (mode, current.entry.as_ref()) {
(NamedAttributeWriteMode::Create, Some(_))
| (NamedAttributeWriteMode::Replace, None) => {
return Err(OperationFailure::new(FsError::CreationRejected, work));
}
_ => {}
}
let blob = self
.stage_blob(value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, blob.work)?;
let replacement = AttributeEntry {
name,
value_bytes: blob.value.logical_bytes,
value: blob.value.root,
};
let mutation = match current.entry {
None => AttributeMutation::Insert(replacement),
Some(expected) => AttributeMutation::Replace {
expected,
replacement,
},
};
let changed = apply_attribute_mutations_async(
&self.volume.fs.inner.objects,
root,
vec![mutation],
1,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeMutation))?;
work = add(work, changed.work)?;
metadata_value.named_attributes = MetadataField::Value(changed.root);
let changed_metadata = self
.set_metadata_by_id(
file_id,
metadata_value,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, changed_metadata.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn remove_named_attribute_by_id(
&mut self,
file_id: FileId,
name: AttributeName,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let metadata = self
.read_metadata_by_id(file_id, budget, cancellation)
.await?;
let mut work = metadata.work;
let mut metadata_value = metadata.value;
let MetadataField::Value(root) = metadata_value.named_attributes else {
return Err(OperationFailure::new(FsError::NotFound, work));
};
let current = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
&name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, current.work)?;
let expected = current
.entry
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let changed = apply_attribute_mutations_async(
&self.volume.fs.inner.objects,
root,
vec![AttributeMutation::Remove {
name,
expected: Some(expected),
}],
1,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeMutation))?;
work = add(work, changed.work)?;
metadata_value.named_attributes = MetadataField::Value(changed.root);
let changed_metadata = self
.set_metadata_by_id(
file_id,
metadata_value,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, changed_metadata.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn write_file_by_id(
&mut self,
file_id: FileId,
offset: u64,
bytes: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if bytes.is_empty() {
return self
.mutate(
vec![Mutation::File {
file_id,
mutation: FileMutation::ValidateRegular,
}],
budget,
cancellation,
)
.await;
}
if u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> self.volume.config.limits.maximum_read_bytes
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let blob = self.stage_blob(bytes, budget, cancellation).await?;
let mut work = blob.work;
let mutation = self
.mutate(
vec![Mutation::File {
file_id,
mutation: FileMutation::Write {
offset,
length: blob.value.logical_bytes,
content: blob.value.root,
content_offset: 0,
},
}],
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn set_metadata_by_id(
&mut self,
file_id: FileId,
metadata: FileMetadata,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let encoded = encode_file_metadata(metadata)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (metadata, mut work) = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encoded,
WorkCounters::default(),
budget,
cancellation,
)
.await?;
let mutation = self
.mutate(
vec![Mutation::File {
file_id,
mutation: FileMutation::SetMetadata { metadata },
}],
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn set_attributes_by_id(
&mut self,
file_id: FileId,
metadata: FileMetadata,
logical_bytes: Option<u64>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let encoded = encode_file_metadata(metadata)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (metadata, mut work) = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encoded,
WorkCounters::default(),
budget,
cancellation,
)
.await?;
let mut operations = vec![Mutation::File {
file_id,
mutation: FileMutation::SetMetadata { metadata },
}];
if let Some(logical_bytes) = logical_bytes {
operations.push(Mutation::File {
file_id,
mutation: FileMutation::Resize { logical_bytes },
});
}
let mutation = self
.mutate(operations, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn resize_file_by_id(
&mut self,
file_id: FileId,
logical_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::File {
file_id,
mutation: FileMutation::Resize { logical_bytes },
}],
budget,
cancellation,
)
.await
}
pub async fn zero_file_range_by_id(
&mut self,
file_id: FileId,
range: ByteRange,
allocated: bool,
extend: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::File {
file_id,
mutation: FileMutation::ZeroRange {
offset: range.offset,
length: range.length,
allocated,
extend,
},
}],
budget,
cancellation,
)
.await
}
pub async fn preallocate_file_by_id(
&mut self,
file_id: FileId,
range: ByteRange,
keep_size: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::File {
file_id,
mutation: FileMutation::Preallocate {
offset: range.offset,
length: range.length,
keep_size,
},
}],
budget,
cancellation,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn clone_file_range_by_id(
&mut self,
source_file_id: FileId,
source_offset: u64,
destination_file_id: FileId,
destination_offset: u64,
length: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::CloneFileRange {
source_file_id,
source_offset,
destination_file_id,
destination_offset,
length,
}],
budget,
cancellation,
)
.await
}
pub async fn remove(
&mut self,
path: NamespacePath,
expected_file_id: Option<FileId>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::Remove {
path,
expected_file_id: expected_file_id
.map_or(MetadataField::Unavailable, MetadataField::Value),
}],
budget,
cancellation,
)
.await
}
pub async fn rename(
&mut self,
source: NamespacePath,
destination: NamespacePath,
replace: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::Rename {
source,
destination,
replace,
}],
budget,
cancellation,
)
.await
}
pub async fn hard_link(
&mut self,
source: NamespacePath,
destination: NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::Link {
source,
destination,
}],
budget,
cancellation,
)
.await
}
pub async fn set_metadata(
&mut self,
path: NamespacePath,
metadata: FileMetadata,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let mut work = WorkCounters::default();
let encoded = encode_file_metadata(metadata)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let stored = self
.volume
.fs
.put_encoded(ObjectKind::Metadata, encoded, work, budget, cancellation)
.await?;
work = stored.1;
let mutation = self
.mutate(
vec![Mutation::SetMetadata {
path,
metadata: stored.0,
}],
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn set_attributes(
&mut self,
path: NamespacePath,
metadata: FileMetadata,
logical_bytes: Option<u64>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let lookup = self
.lookup_no_follow_observed(&path, budget, cancellation, true)
.await?;
let mut work = lookup.work;
let file_id = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?
.file_id;
let mutation = self
.set_attributes_by_id(
file_id,
metadata,
logical_bytes,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn resize_file(
&mut self,
path: NamespacePath,
logical_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::Resize {
path,
logical_bytes,
}],
budget,
cancellation,
)
.await
}
pub async fn zero_file_range(
&mut self,
path: NamespacePath,
range: ByteRange,
allocated: bool,
extend: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::ZeroRange {
path,
offset: range.offset,
length: range.length,
allocated,
extend,
}],
budget,
cancellation,
)
.await
}
pub async fn preallocate_file(
&mut self,
path: NamespacePath,
range: ByteRange,
keep_size: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::Preallocate {
path,
offset: range.offset,
length: range.length,
keep_size,
}],
budget,
cancellation,
)
.await
}
pub async fn clone_file_range(
&mut self,
request: FileCloneRequest,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate(
vec![Mutation::CloneRange {
source: request.source,
source_offset: request.source_offset,
destination: request.destination,
destination_offset: request.destination_offset,
length: request.length,
}],
budget,
cancellation,
)
.await
}
pub async fn stage_content<R: AsyncBlobSource>(
&self,
source: &mut R,
maximum_source_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<StagedContent> {
stage_content_for_volume(
&self.volume,
source,
maximum_source_bytes,
budget,
cancellation,
)
.await
}
async fn stage_blob(
&self,
bytes: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<crate::kernel::BlobBuild> {
if u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> self.volume.config.limits.maximum_read_bytes
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let maximum_blob_bytes = u64::try_from(bytes.len()).unwrap_or(u64::MAX).max(1);
let mut source = std::io::Cursor::new(bytes);
self.stage_blob_source(&mut source, maximum_blob_bytes, budget, cancellation)
.await
}
async fn stage_blob_source<R: AsyncBlobSource>(
&self,
source: &mut R,
maximum_blob_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<crate::kernel::BlobBuild> {
let staged = stage_content_for_volume(
&self.volume,
source,
maximum_blob_bytes,
budget,
cancellation,
)
.await?;
Ok(FsReceipt {
work: staged.work,
value: crate::kernel::BlobBuild {
root: staged.value.root,
logical_bytes: staged.value.logical_bytes,
work: staged.work,
},
})
}
pub async fn commit(
&mut self,
operation_id: OperationId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
if self.mode.access != AccessMode::ReadWrite
|| self.mode.mutations != MutationMode::PrivateOverlay
{
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
self.publish_pending(operation_id, None, budget, cancellation)
.await
}
pub async fn commit_with_permit(
&mut self,
operation_id: OperationId,
permit: PublicationPermit,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
if self.mode.access != AccessMode::ReadWrite
|| self.mode.mutations != MutationMode::PrivateOverlay
{
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
self.publish_pending_with_permit(operation_id, None, permit, budget, cancellation)
.await
}
#[cfg(feature = "native-mount")]
pub(crate) async fn commit_with_permit_even_if_clean(
&mut self,
operation_id: OperationId,
permit: PublicationPermit,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
if self.mode.access != AccessMode::ReadWrite
|| self.mode.mutations != MutationMode::PrivateOverlay
{
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
let expected = self
.authority_head
.ok_or_else(|| OperationFailure::before_work(FsError::WritableCheckoutRequiresHead))?;
self.publish_pending_against(operation_id, None, permit, expected, budget, cancellation)
.await
}
#[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
pub(crate) async fn commit_with_context(
&mut self,
operation_id: OperationId,
operation_context: Digest,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
if self.mode.access != AccessMode::ReadWrite
|| self.mode.mutations != MutationMode::PrivateOverlay
{
return Err(OperationFailure::before_work(FsError::MutationNotAllowed));
}
let expected = self
.authority_head
.ok_or_else(|| OperationFailure::before_work(FsError::WritableCheckoutRequiresHead))?;
self.publish_pending_against(
operation_id,
Some(operation_context),
PublicationPermit::Unrestricted,
expected,
budget,
cancellation,
)
.await
}
async fn publish_pending(
&mut self,
operation_id: OperationId,
operation_context: Option<Digest>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
self.publish_pending_with_permit(
operation_id,
operation_context,
PublicationPermit::Unrestricted,
budget,
cancellation,
)
.await
}
async fn publish_pending_with_permit(
&mut self,
operation_id: OperationId,
operation_context: Option<Digest>,
permit: PublicationPermit,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
if !self.has_pending_mutations() && permit == PublicationPermit::Unrestricted {
return self
.resolve_clean_commit(operation_id, budget, cancellation)
.await;
}
let expected = self
.authority_head
.ok_or_else(|| OperationFailure::before_work(FsError::WritableCheckoutRequiresHead))?;
self.publish_pending_against(
operation_id,
operation_context,
permit,
expected,
budget,
cancellation,
)
.await
}
async fn publish_pending_against(
&mut self,
operation_id: OperationId,
operation_context: Option<Digest>,
permit: PublicationPermit,
expected: Head,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
let checkpoint = self.checkpoint_root(budget, cancellation).await?;
let checkpoint_root = checkpoint.value;
let mut work = checkpoint.work;
let request = PublishGenerationRequest {
authority_id: volume_authority_id(self.volume.id),
volume_id: self.volume.id,
epoch: expected.epoch,
expected,
operation_id,
generation_root: checkpoint_root,
};
let publication = if permit != PublicationPermit::Unrestricted {
publish_generation_async_with_permit(
&self.volume.fs.inner.objects,
&self.volume.fs.inner.authority,
request,
PublicationIntent {
operation_context,
permit,
},
closure_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
} else if let Some(context) = operation_context {
publish_generation_async_with_context(
&self.volume.fs.inner.objects,
&self.volume.fs.inner.authority,
request,
context,
closure_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
} else {
publish_generation_async(
&self.volume.fs.inner.objects,
&self.volume.fs.inner.authority,
request,
closure_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
}
.map_err(|failure| failure.map_with_prior_work(work, Into::into))?;
work = add(work, publication.work)?;
let outcome = self.apply_publication(checkpoint_root, operation_id, publication);
Ok(FsReceipt {
value: outcome,
work,
})
}
fn apply_publication(
&mut self,
checkpoint_root: ObjectId,
operation_id: OperationId,
publication: PublicationReceipt,
) -> CheckoutCommitOutcome {
let generation_id = publication.proof.generation_id;
match publication.outcome {
AppendOutcome::Committed(commit) => {
let head = durable_head(&commit);
self.base_generation_root = checkpoint_root;
self.generation_root = checkpoint_root;
self.root = publication.proof.root;
self.base_root = self.root.clone();
self.base_file_table = self.root.file_table;
self.authority_head = Some(head);
self.prepared_merge_parent = None;
self.dependencies.clear();
self.last_commit = Some(LastCommit {
operation_id,
generation_id,
head,
});
CheckoutCommitOutcome::Committed {
generation_id,
head,
}
}
AppendOutcome::AlreadyCommitted(commit) => {
let head = durable_head(&commit);
self.base_generation_root = checkpoint_root;
self.generation_root = checkpoint_root;
self.root = publication.proof.root;
self.base_root = self.root.clone();
self.base_file_table = self.root.file_table;
self.authority_head = Some(head);
self.prepared_merge_parent = None;
self.dependencies.clear();
self.last_commit = Some(LastCommit {
operation_id,
generation_id,
head,
});
CheckoutCommitOutcome::AlreadyCommitted {
generation_id,
head,
}
}
AppendOutcome::Conflict { actual } => CheckoutCommitOutcome::Conflict { actual },
AppendOutcome::Fenced { actual_epoch } => {
CheckoutCommitOutcome::Fenced { actual_epoch }
}
AppendOutcome::IdempotencyConflict {
committed_fingerprint,
} => CheckoutCommitOutcome::IdempotencyConflict {
committed_fingerprint,
},
}
}
pub(crate) async fn retry_stale_commit(
&mut self,
operation_id: OperationId,
permit: PublicationPermit,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<CheckoutCommitOutcome>> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
let resolved = self
.volume
.fs
.inner
.authority
.find_operation(
volume_authority_id(self.volume.id),
operation_id,
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(FsError::Authority(failure.error), *failure.work)
})?;
let Some(commit) = resolved.value else {
return Ok(FsReceipt {
value: None,
work: resolved.work,
});
};
let previous_sequence = commit.sequence.get().checked_sub(1).ok_or_else(|| {
OperationFailure::new(FsError::InvalidAuthorityHistory, resolved.work)
})?;
let expected = Head {
epoch: commit.epoch,
sequence: Sequence::new(previous_sequence),
digest: commit.previous_digest,
};
let retried = self
.publish_pending_against(
operation_id,
None,
permit,
expected,
remaining(resolved.work, budget)?,
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(resolved.work, std::convert::identity)
})?;
Ok(FsReceipt {
value: Some(retried.value),
work: add(resolved.work, retried.work)?,
})
}
async fn resolve_clean_commit(
&self,
operation_id: OperationId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<CheckoutCommitOutcome> {
if let Some(last) = self.last_commit {
if last.operation_id != operation_id {
return Err(OperationFailure::before_work(FsError::NoPendingMutations));
}
if last.generation_id == GenerationId::new(self.generation_root.digest) {
return Ok(FsReceipt {
value: CheckoutCommitOutcome::AlreadyCommitted {
generation_id: last.generation_id,
head: last.head,
},
work: WorkCounters::default(),
});
}
}
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
let head = self
.authority_head
.ok_or_else(|| OperationFailure::before_work(FsError::WritableCheckoutRequiresHead))?;
let resolved = self
.volume
.fs
.inner
.authority
.find_operation(
volume_authority_id(self.volume.id),
operation_id,
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(FsError::Authority(failure.error), *failure.work)
})?;
let Some(commit) = resolved.value else {
return Err(OperationFailure::new(
FsError::NoPendingMutations,
resolved.work,
));
};
let generation_root = generation_from_record(&commit, self.volume.id, resolved.work)?;
let value = if generation_root == self.generation_root {
CheckoutCommitOutcome::AlreadyCommitted {
generation_id: GenerationId::new(generation_root.digest),
head,
}
} else {
CheckoutCommitOutcome::IdempotencyConflict {
committed_fingerprint: commit.fingerprint,
}
};
Ok(FsReceipt {
value,
work: resolved.work,
})
}
pub async fn lookup_no_follow(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<PathLookup> {
self.lookup_no_follow_observed(path, budget, cancellation, true)
.await
}
pub async fn lookup_no_follow_with_metadata(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<PathMetadataLookup>> {
let lookup = self
.lookup_no_follow_observed(path, budget, cancellation, false)
.await?;
let mut work = lookup.work;
let Some(record) = lookup.value.record else {
return Ok(FsReceipt { value: None, work });
};
let metadata = self
.volume
.fs
.inner
.objects
.read(
record.metadata,
self.volume.config.limits.maximum_object_bytes,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Object))?;
work = add(work, metadata.work)?;
let metadata = decode_file_metadata(&metadata.value, decode_limits(self.volume.config))
.map_err(|error| OperationFailure::new(error.into(), work))?;
if self.mode.consistency != ConsistencyMode::Pinned {
work = self
.observe_base_metadata_ids(
std::slice::from_ref(&record.file_id),
work,
budget,
cancellation,
)
.await?;
}
Ok(FsReceipt {
value: Some(PathMetadataLookup { record, metadata }),
work,
})
}
async fn lookup_no_follow_observed(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
capture_terminal: bool,
) -> FsResult<PathLookup> {
let synchronized = self.synchronize_live(budget, cancellation).await?;
let prior = synchronized.work;
let mut lookup = Box::pin(self.lookup_no_follow_at_current(
path,
remaining(prior, budget)?,
cancellation,
capture_terminal,
))
.await
.map_err(|failure| failure.map_with_prior_work(prior, std::convert::identity))?;
let mut total = add(prior, lookup.work)?;
if lookup.value.record.is_none()
&& self.volume.config.case_sensitivity == CaseSensitivity::ProfileFolded
{
let folded = Box::pin(self.resolve_profile_folded_path(
path,
remaining(total, budget)?,
cancellation,
capture_terminal,
))
.await
.map_err(|failure| failure.map_with_prior_work(total, std::convert::identity))?;
total = add(total, folded.work)?;
if let Some(resolved) = folded.value {
lookup.value = resolved;
}
}
lookup.work = total;
lookup.value.work = total;
Ok(lookup)
}
async fn resolve_profile_folded_path(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
capture_terminal: bool,
) -> FsResult<Option<PathLookup>> {
let mut work = WorkCounters::default();
if path.is_root() {
return Ok(FsReceipt { value: None, work });
}
let resolved = Box::pin(self.canonicalize_path_components(
path.components(),
&[],
budget,
cancellation,
))
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, resolved.work)?;
let Some(corrected) = resolved.value else {
return Ok(FsReceipt { value: None, work });
};
let retried = self
.lookup_no_follow_at_current(
&corrected,
remaining(work, budget)?,
cancellation,
capture_terminal,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, retried.work)?;
Ok(FsReceipt {
value: Some(retried.value),
work,
})
}
async fn lookup_no_follow_at_current(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
capture_terminal: bool,
) -> FsResult<PathLookup> {
let lookup = if self.mode.consistency == ConsistencyMode::Pinned {
crate::kernel::lookup_path_async(
&self.volume.fs.inner.objects,
&self.root,
path,
self.volume.config,
budget,
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?
} else {
let candidate = if self.has_pending_mutations() {
Some(
crate::kernel::lookup_path_async(
&self.volume.fs.inner.objects,
&self.root,
path,
self.volume.config,
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(failure.error.into(), *failure.work)
})?,
)
} else {
None
};
let prior = candidate
.as_ref()
.map_or(WorkCounters::default(), |value| value.work);
let observed = self
.observe_base_for_candidate(
path,
prior,
candidate.is_some(),
capture_terminal,
budget,
cancellation,
)
.await?;
let combined = observed.work;
self.dependencies
.extend_observations(
observed.value.dependencies,
self.volume.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), combined))?;
candidate.map_or(
PathLookup {
work: combined,
..observed.value.lookup
},
|value| PathLookup {
work: combined,
..value
},
)
};
Ok(FsReceipt {
work: lookup.work,
value: lookup,
})
}
async fn observe_base_for_candidate(
&self,
path: &NamespacePath,
prior: WorkCounters,
has_candidate: bool,
capture_terminal: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<crate::kernel::ObservedPathLookup> {
let observed = if capture_terminal {
crate::kernel::observe_path_async(
&self.volume.fs.inner.objects,
&self.base_root,
path,
self.volume.config,
remaining(prior, budget)?,
cancellation,
)
.await
} else {
crate::kernel::observe_path_edges_async(
&self.volume.fs.inner.objects,
&self.base_root,
path,
self.volume.config,
remaining(prior, budget)?,
cancellation,
)
.await
};
let (observed, observation_prior) = match observed {
Ok(observed) => (observed, prior),
Err(failure)
if has_candidate && matches!(failure.error, PathLookupError::NotDirectory) =>
{
let consumed = add(prior, *failure.work)?;
let observed = crate::kernel::observe_path_edges_async(
&self.volume.fs.inner.objects,
&self.base_root,
path,
self.volume.config,
remaining(consumed, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(consumed, FsError::Path))?;
(observed, consumed)
}
Err(failure) => return Err(failure.map_with_prior_work(prior, FsError::Path)),
};
Ok(FsReceipt {
work: add(observation_prior, observed.lookup.work)?,
value: observed,
})
}
pub async fn lookup_batch_no_follow(
&mut self,
paths: &[NamespacePath],
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<PathBatchLookup> {
let synchronized = self.synchronize_live(budget, cancellation).await?;
let prior = synchronized.work;
let mut lookup = crate::kernel::lookup_paths_async(
&self.volume.fs.inner.objects,
&self.root,
paths,
self.volume.config,
remaining(prior, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(prior, FsError::Path))?;
lookup.work = add(prior, lookup.work)?;
if self.mode.consistency != ConsistencyMode::Pinned {
let mut work = lookup.work;
for path in paths {
let observed = crate::kernel::observe_path_async(
&self.volume.fs.inner.objects,
&self.base_root,
path,
self.volume.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Path))?;
work = add(work, observed.lookup.work)?;
self.dependencies
.extend_observations(
observed.dependencies,
self.volume.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
}
lookup.work = work;
}
Ok(FsReceipt {
work: lookup.work,
value: lookup,
})
}
pub async fn list_directory(
&mut self,
path: &NamespacePath,
after: Option<&LogicalName>,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryPage> {
self.list_directory_with_bound(path, after, false, maximum_entries, budget, cancellation)
.await
}
async fn list_directory_with_bound(
&mut self,
path: &NamespacePath,
after: Option<&LogicalName>,
inclusive: bool,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryPage> {
let lookup = self
.lookup_no_follow_observed(path, budget, cancellation, false)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let FilePayload::Directory { entries } = record.payload else {
return Err(OperationFailure::new(FsError::NotDirectory, work));
};
if record.kind != FileKind::Directory {
return Err(OperationFailure::new(FsError::NotDirectory, work));
}
let mut page = if let Some(at) = after.filter(|_| inclusive) {
list_tree_entries_at_or_after_async(
&self.volume.fs.inner.objects,
entries,
at,
maximum_entries,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
} else {
list_tree_entries_async(
&self.volume.fs.inner.objects,
entries,
after,
maximum_entries,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
}
.map_err(|failure| failure.map_with_prior_work(work, FsError::Directory))?;
work = add(work, page.work)?;
if self.mode.consistency != ConsistencyMode::Pinned {
let region = DependencyRegion::DirectoryRange {
directory_id: record.file_id,
after: after.cloned(),
maximum_entries,
};
let probe = AuthenticatedGenerationProbe::new(
&self.volume.fs.inner.objects,
probe_limits(self.volume.config),
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let state = probe
.probe_async(
GenerationId::new(self.base_generation_root.digest),
®ion,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Probe))?;
work = add(work, state.work)?;
self.dependencies
.extend_observations(
vec![Dependency {
region,
expected: state.value,
}],
self.volume.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
}
page.work = work;
Ok(FsReceipt { value: page, work })
}
pub async fn list_directory_records(
&mut self,
path: &NamespacePath,
after: Option<&LogicalName>,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryRecordPage> {
self.list_directory_records_with_bound(
path,
after,
false,
maximum_entries,
budget,
cancellation,
)
.await
}
pub(crate) async fn list_directory_records_at_or_after(
&mut self,
path: &NamespacePath,
at: &LogicalName,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryRecordPage> {
if self.mode.consistency != ConsistencyMode::Pinned {
return Err(OperationFailure::before_work(FsError::UnsupportedCheckout));
}
self.list_directory_records_with_bound(
path,
Some(at),
true,
maximum_entries,
budget,
cancellation,
)
.await
}
async fn list_directory_records_with_bound(
&mut self,
path: &NamespacePath,
after: Option<&LogicalName>,
inclusive: bool,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryRecordPage> {
let listing = self
.list_directory_with_bound(
path,
after,
inclusive,
maximum_entries,
budget,
cancellation,
)
.await?;
let mut work = listing.work;
let page = listing.value;
if page.entries.is_empty() {
return Ok(FsReceipt {
value: DirectoryRecordPage {
entries: Vec::new(),
has_more: page.has_more,
},
work,
});
}
let retained = u64::try_from(page.entries.len())
.unwrap_or(u64::MAX)
.saturating_mul(u64::try_from(size_of::<FileId>()).unwrap_or(u64::MAX));
work = add(
work,
WorkCounters {
allocation_operations: 1,
peak_allocation_bytes: retained,
..WorkCounters::default()
},
)?;
work.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let mut file_ids = Vec::new();
file_ids
.try_reserve_exact(page.entries.len())
.map_err(|_| OperationFailure::new(FsError::PendingMutationAllocationFailed, work))?;
file_ids.extend(page.entries.iter().map(|entry| entry.file_id));
let records = lookup_file_records_async(
&self.volume.fs.inner.objects,
self.root.file_table,
&file_ids,
maximum_entries,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRecord))?;
work = add(work, records.work)?;
drop(file_ids);
let mut named_records = Vec::new();
named_records
.try_reserve_exact(page.entries.len())
.map_err(|_| OperationFailure::new(FsError::PendingMutationAllocationFailed, work))?;
for (binding, record) in page.entries.into_iter().zip(records.records) {
let record = record
.ok_or_else(|| OperationFailure::new(FsError::InvalidDirectoryRecord, work))?;
if record.file_id != binding.file_id || record.kind != binding.kind {
return Err(OperationFailure::new(FsError::InvalidDirectoryRecord, work));
}
named_records.push((binding.name, record));
}
self.attach_directory_metadata(named_records, page.has_more, work, budget, cancellation)
.await
}
async fn attach_directory_metadata(
&mut self,
named_records: Vec<(LogicalName, FileRecord)>,
has_more: bool,
mut work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<DirectoryRecordPage> {
let mut requests = Vec::new();
requests
.try_reserve_exact(named_records.len())
.map_err(|_| OperationFailure::new(FsError::PendingMutationAllocationFailed, work))?;
requests.extend(named_records.iter().map(|(_, record)| ObjectReadRequest {
object_id: record.metadata,
maximum_bytes: self.volume.config.limits.maximum_object_bytes,
}));
let metadata_reads = self
.volume
.fs
.inner
.objects
.read_many(&requests, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Object))?;
work = add(work, metadata_reads.work)?;
let mut entries = Vec::new();
entries
.try_reserve_exact(named_records.len())
.map_err(|_| OperationFailure::new(FsError::PendingMutationAllocationFailed, work))?;
for ((name, record), metadata) in named_records.into_iter().zip(metadata_reads.value) {
let metadata = decode_file_metadata(&metadata, decode_limits(self.volume.config))
.map_err(|error| OperationFailure::new(error.into(), work))?;
entries.push(DirectoryRecordEntry {
name,
record,
metadata,
});
}
if self.mode.consistency != ConsistencyMode::Pinned && !entries.is_empty() {
let mut file_ids = Vec::new();
file_ids.try_reserve_exact(entries.len()).map_err(|_| {
OperationFailure::new(FsError::PendingMutationAllocationFailed, work)
})?;
file_ids.extend(entries.iter().map(|entry| entry.record.file_id));
work = self
.observe_base_metadata_ids(&file_ids, work, budget, cancellation)
.await?;
}
Ok(FsReceipt {
value: DirectoryRecordPage { entries, has_more },
work,
})
}
pub async fn read_metadata(
&mut self,
path: &NamespacePath,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileMetadata> {
let lookup = self
.lookup_no_follow_with_metadata(path, budget, cancellation)
.await?;
let value = lookup
.value
.ok_or_else(|| OperationFailure::new(FsError::NotFound, lookup.work))?
.metadata;
Ok(FsReceipt {
value,
work: lookup.work,
})
}
pub async fn read_named_attribute(
&mut self,
path: &NamespacePath,
name: &AttributeName,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<Bytes>> {
let metadata = self.read_metadata(path, budget, cancellation).await?;
let mut work = metadata.work;
let MetadataField::Value(root) = metadata.value.named_attributes else {
return Ok(FsReceipt { value: None, work });
};
let lookup = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, lookup.work)?;
let Some(entry) = lookup.entry else {
return Ok(FsReceipt { value: None, work });
};
if entry.value_bytes > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::InvalidRange),
work,
));
}
let read = read_blob_range_async(
&self.volume.fs.inner.objects,
entry.value,
ByteRange {
offset: 0,
length: entry.value_bytes,
},
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::BlobRead))?;
work = add(work, read.work)?;
Ok(FsReceipt {
value: Some(read.bytes),
work,
})
}
pub async fn list_named_attributes(
&mut self,
path: &NamespacePath,
after: Option<&AttributeName>,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<AttributeListing> {
let metadata = self.read_metadata(path, budget, cancellation).await?;
let mut work = metadata.work;
let MetadataField::Value(root) = metadata.value.named_attributes else {
return Ok(FsReceipt {
value: AttributeListing {
entries: Vec::new(),
has_more: false,
next_residency: None,
work,
},
work,
});
};
let mut listing = list_attributes_async(
&self.volume.fs.inner.objects,
root,
after,
maximum_entries,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeList))?;
work = add(work, listing.work)?;
listing.work = work;
Ok(FsReceipt {
value: listing,
work,
})
}
pub async fn write_named_attribute(
&mut self,
path: NamespacePath,
name: AttributeName,
value: Bytes,
mode: NamedAttributeWriteMode,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let metadata = self.read_metadata(&path, budget, cancellation).await?;
let mut work = metadata.work;
let mut metadata_value = metadata.value;
let root = match metadata_value.named_attributes {
MetadataField::Unavailable => {
let empty = self
.stage_empty_attributes(remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, empty.work)?;
empty.value
}
MetadataField::Value(root) => root,
};
let current = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
&name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, current.work)?;
match (mode, current.entry.as_ref()) {
(NamedAttributeWriteMode::Create, Some(_))
| (NamedAttributeWriteMode::Replace, None) => {
return Err(OperationFailure::new(FsError::CreationRejected, work));
}
_ => {}
}
let blob = self
.stage_blob(value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, blob.work)?;
let replacement = AttributeEntry {
name,
value_bytes: blob.value.logical_bytes,
value: blob.value.root,
};
let mutation = match current.entry {
None => AttributeMutation::Insert(replacement),
Some(expected) => AttributeMutation::Replace {
expected,
replacement,
},
};
let changed = apply_attribute_mutations_async(
&self.volume.fs.inner.objects,
root,
vec![mutation],
1,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeMutation))?;
work = add(work, changed.work)?;
metadata_value.named_attributes = MetadataField::Value(changed.root);
let changed_metadata = self
.set_metadata(path, metadata_value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, changed_metadata.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn remove_named_attribute(
&mut self,
path: NamespacePath,
name: AttributeName,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let metadata = self.read_metadata(&path, budget, cancellation).await?;
let mut work = metadata.work;
let mut metadata_value = metadata.value;
let MetadataField::Value(root) = metadata_value.named_attributes else {
return Err(OperationFailure::new(FsError::NotFound, work));
};
let current = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
&name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, current.work)?;
let expected = current
.entry
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let changed = apply_attribute_mutations_async(
&self.volume.fs.inner.objects,
root,
vec![AttributeMutation::Remove {
name,
expected: Some(expected),
}],
1,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeMutation))?;
work = add(work, changed.work)?;
metadata_value.named_attributes = MetadataField::Value(changed.root);
let changed_metadata = self
.set_metadata(path, metadata_value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, changed_metadata.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn plan_file_extents(
&mut self,
path: &NamespacePath,
range: ByteRange,
maximum_spans: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<ExtentPlan>> {
if maximum_spans == 0 || range.length > self.volume.config.limits.maximum_generation_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let lookup = self
.lookup_no_follow_observed(path, budget, cancellation, false)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let (logical_bytes, extents) = match record.payload {
FilePayload::InlineRegular(data) => {
let logical_bytes = u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX);
validate_planned_file_range(logical_bytes, range, work)?;
if self.mode.consistency != ConsistencyMode::Pinned && range.length != 0 {
work = self
.observe_base_regular_range(
record.file_id,
range,
work,
budget,
cancellation,
)
.await?;
}
return Ok(FsReceipt { value: None, work });
}
FilePayload::Regular {
logical_bytes,
extents,
} => (logical_bytes, extents),
FilePayload::Directory { .. }
| FilePayload::SymbolicLink { .. }
| FilePayload::Device { .. }
| FilePayload::Empty
| FilePayload::ReparsePoint { .. } => {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
work,
));
}
};
let mut plan = plan_extent_range_async(
&self.volume.fs.inner.objects,
ExtentRangeRequest {
root: extents,
file_size: logical_bytes,
range,
maximum_spans,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, |error| {
FsError::FileRead(FileRangeReadError::Extent(error))
})
})?;
work = add(work, plan.work)?;
if self.mode.consistency != ConsistencyMode::Pinned && range.length != 0 {
work = self
.observe_base_regular_range(record.file_id, range, work, budget, cancellation)
.await?;
}
plan.work = work;
Ok(FsReceipt {
value: Some(plan),
work,
})
}
pub async fn seek_file_extent(
&mut self,
path: &NamespacePath,
offset: u64,
target: ExtentSeekTarget,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<u64>> {
let lookup = self
.lookup_no_follow_observed(path, budget, cancellation, false)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let value = match record.payload {
FilePayload::InlineRegular(data) => {
let logical_bytes = u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX);
if offset > logical_bytes {
None
} else {
match target {
ExtentSeekTarget::Data => (offset < logical_bytes).then_some(offset),
ExtentSeekTarget::Hole => Some(logical_bytes),
}
}
}
FilePayload::Regular {
logical_bytes,
extents,
} => {
if offset > logical_bytes {
None
} else {
let receipt = seek_extent_async(
&self.volume.fs.inner.objects,
ExtentSeekRequest {
root: extents,
file_size: logical_bytes,
offset,
target,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| {
failure.map_with_prior_work(work, |error| {
FsError::FileRead(FileRangeReadError::Extent(error))
})
})?;
work = add(work, receipt.work)?;
receipt.value
}
}
FilePayload::Directory { .. }
| FilePayload::SymbolicLink { .. }
| FilePayload::Empty
| FilePayload::Device { .. }
| FilePayload::ReparsePoint { .. } => {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::NotRegular),
work,
));
}
};
if self.mode.consistency != ConsistencyMode::Pinned {
let region = DependencyRegion::SparseSeek {
file_id: record.file_id,
offset,
target,
};
let probe = AuthenticatedGenerationProbe::new(
&self.volume.fs.inner.objects,
probe_limits(self.volume.config),
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let state = probe
.probe_async(
GenerationId::new(self.base_generation_root.digest),
®ion,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::Probe))?;
work = add(work, state.work)?;
self.dependencies
.extend_observations(
vec![Dependency {
region,
expected: state.value,
}],
self.volume.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
}
Ok(FsReceipt { value, work })
}
pub async fn read_file_range(
&mut self,
path: &NamespacePath,
range: ByteRange,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
if range.length > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let lookup = self
.lookup_no_follow_observed(path, budget, cancellation, false)
.await?;
let mut work = lookup.work;
let record = lookup
.value
.record
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let mut read = read_file_range_async(
&self.volume.fs.inner.objects,
FileRangeRequest {
record,
range,
maximum_spans: self.volume.config.limits.maximum_directory_page_entries,
limits: decode_limits(self.volume.config),
budget: remaining(work, budget)?,
},
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRead))?;
work = add(work, read.work)?;
if self.mode.consistency != ConsistencyMode::Pinned && range.length != 0 {
work = self
.observe_base_regular_range(record.file_id, range, work, budget, cancellation)
.await?;
}
read.work = work;
Ok(FsReceipt { value: read, work })
}
async fn lookup_file_record_by_id(
&self,
file_id: FileId,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<FileRecord>> {
let lookup = lookup_file_record_async(
&self.volume.fs.inner.objects,
self.root.file_table,
file_id,
decode_limits(self.volume.config),
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(FsError::FileRecord(failure.error), *failure.work)
})?;
Ok(FsReceipt {
value: lookup.record,
work: lookup.work,
})
}
async fn observe_identity_region(
&mut self,
region: DependencyRegion,
work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<WorkCounters, OperationFailure<FsError>> {
self.observe_identity_regions(vec![region], work, budget, cancellation)
.await
}
async fn observe_identity_regions(
&mut self,
regions: Vec<DependencyRegion>,
mut work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<WorkCounters, OperationFailure<FsError>> {
let captured = capture_dependencies_async(
&self.volume.fs.inner.objects,
&self.base_generation_root,
self.volume.config,
regions,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, captured.work)?;
self.dependencies
.extend_observations(
captured.value,
self.volume.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
Ok(work)
}
async fn observe_base_regular_range(
&mut self,
file_id: FileId,
range: ByteRange,
mut work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<WorkCounters, OperationFailure<FsError>> {
let lookup = lookup_file_record_async(
&self.volume.fs.inner.objects,
self.base_root.file_table,
file_id,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRecord))?;
work = add(work, lookup.work)?;
let regions = regular_range_regions(lookup.record, range.offset, range.length)
.into_iter()
.flatten()
.collect();
self.observe_identity_regions(regions, work, budget, cancellation)
.await
}
async fn observe_base_metadata_ids(
&mut self,
file_ids: &[FileId],
mut work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<WorkCounters, OperationFailure<FsError>> {
if file_ids.is_empty() {
return Ok(work);
}
let maximum = u32::try_from(file_ids.len()).unwrap_or(u32::MAX);
let records = lookup_file_records_async(
&self.volume.fs.inner.objects,
self.base_root.file_table,
file_ids,
maximum,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::FileRecord))?;
work = add(work, records.work)?;
let mut dependencies = Vec::new();
dependencies
.try_reserve_exact(file_ids.len())
.map_err(|_| OperationFailure::new(FsError::PendingMutationAllocationFailed, work))?;
dependencies.extend(
file_ids
.iter()
.zip(records.records)
.map(|(file_id, record)| Dependency {
region: DependencyRegion::Metadata(*file_id),
expected: record.map_or(DependencyState::Absent, |record| {
DependencyState::Present(record.metadata.digest)
}),
}),
);
self.dependencies
.extend_observations(
dependencies,
self.volume.config.limits.maximum_checkout_dependencies,
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
Ok(work)
}
}
async fn stage_content_for_volume<
A: AsyncAuthorityStore,
O: AsyncObjectStore,
R: AsyncBlobSource,
>(
volume: &Volume<A, O>,
source: &mut R,
maximum_source_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<StagedContent> {
if maximum_source_bytes == 0
|| maximum_source_bytes > volume.config.limits.maximum_generation_bytes
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let chunk_bytes = u32::try_from(volume.config.limits.maximum_object_bytes.min(1024 * 1024))
.unwrap_or(u32::MAX)
.max(1);
let page_bytes = u32::try_from(volume.config.limits.maximum_object_bytes)
.unwrap_or(u32::MAX)
.max(1);
let blob = build_blob_async(
&volume.fs.inner.objects,
source,
BlobBuildOptions {
chunk_bytes,
page_items: volume.config.limits.maximum_directory_page_entries,
page_bytes,
maximum_blob_bytes: maximum_source_bytes,
},
budget,
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
Ok(FsReceipt {
value: StagedContent {
root: blob.root,
logical_bytes: blob.logical_bytes,
},
work: blob.work,
})
}
impl<A: AsyncAuthorityStore, O: AsyncObjectStore> DetachedFile<A, O> {
pub(crate) fn from_record(volume: Volume<A, O>, record: FileRecord) -> Self {
Self { volume, record }
}
#[cfg(feature = "native-mount")]
pub(crate) const fn record(&self) -> FileRecord {
self.record
}
pub(crate) async fn read_symbolic_link(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Bytes> {
read_symbolic_link_record(&self.volume, self.record, budget, cancellation).await
}
#[must_use]
pub const fn file_id(&self) -> FileId {
self.record.file_id
}
#[must_use]
pub fn logical_bytes(&self) -> u64 {
match self.record.payload {
FilePayload::InlineRegular(data) => {
u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX)
}
FilePayload::Regular { logical_bytes, .. } => logical_bytes,
_ => 0,
}
}
pub async fn seek(
&self,
offset: u64,
target: ExtentSeekTarget,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<u64>> {
let value = match self.record.payload {
FilePayload::InlineRegular(data) => {
let logical_bytes = u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX);
if offset > logical_bytes {
None
} else {
match target {
ExtentSeekTarget::Data => (offset < logical_bytes).then_some(offset),
ExtentSeekTarget::Hole => Some(logical_bytes),
}
}
}
FilePayload::Regular {
logical_bytes,
extents,
} => {
if offset > logical_bytes {
None
} else {
return seek_extent_async(
&self.volume.fs.inner.objects,
ExtentSeekRequest {
root: extents,
file_size: logical_bytes,
offset,
target,
limits: decode_limits(self.volume.config),
budget,
},
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(
FsError::FileRead(FileRangeReadError::Extent(failure.error)),
*failure.work,
)
});
}
}
FilePayload::Directory { .. }
| FilePayload::SymbolicLink { .. }
| FilePayload::Empty
| FilePayload::Device { .. }
| FilePayload::ReparsePoint { .. } => {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::NotRegular,
)));
}
};
Ok(FsReceipt {
value,
work: WorkCounters::default(),
})
}
pub async fn set_metadata(
&mut self,
metadata: FileMetadata,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let encoded = encode_file_metadata(metadata)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (metadata, work) = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encoded,
WorkCounters::default(),
budget,
cancellation,
)
.await?;
self.record.metadata = metadata;
Ok(FsReceipt { value: (), work })
}
pub async fn set_attributes(
&mut self,
metadata: FileMetadata,
logical_bytes: Option<u64>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let encoded = encode_file_metadata(metadata)
.map_err(|error| OperationFailure::before_work(error.into()))?;
let (metadata_id, mut work) = self
.volume
.fs
.put_encoded(
ObjectKind::Metadata,
encoded,
WorkCounters::default(),
budget,
cancellation,
)
.await?;
let payload = if let Some(logical_bytes) = logical_bytes {
let resized = apply_regular_mutation_async(
&self.volume.fs.inner.objects,
self.record.payload,
RegularMutation::Resize { logical_bytes },
self.volume.config,
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::DetachedMutation))?;
work = add(work, resized.work)?;
resized.payload
} else {
self.record.payload
};
self.record.metadata = metadata_id;
self.record.payload = payload;
Ok(FsReceipt { value: (), work })
}
pub async fn read_named_attribute(
&self,
name: &AttributeName,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Option<Bytes>> {
let metadata = self.read_metadata(budget, cancellation).await?;
let mut work = metadata.work;
let MetadataField::Value(root) = metadata.value.named_attributes else {
return Ok(FsReceipt { value: None, work });
};
let lookup = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, lookup.work)?;
let Some(entry) = lookup.entry else {
return Ok(FsReceipt { value: None, work });
};
if entry.value_bytes > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::InvalidRange),
work,
));
}
let read = read_blob_range_async(
&self.volume.fs.inner.objects,
entry.value,
ByteRange {
offset: 0,
length: entry.value_bytes,
},
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::BlobRead))?;
work = add(work, read.work)?;
Ok(FsReceipt {
value: Some(read.bytes),
work,
})
}
pub async fn list_named_attributes(
&self,
after: Option<&AttributeName>,
maximum_entries: u32,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<AttributeListing> {
let metadata = self.read_metadata(budget, cancellation).await?;
let mut work = metadata.work;
let MetadataField::Value(root) = metadata.value.named_attributes else {
let value = AttributeListing {
entries: Vec::new(),
has_more: false,
next_residency: None,
work,
};
return Ok(FsReceipt { value, work });
};
let mut listing = list_attributes_async(
&self.volume.fs.inner.objects,
root,
after,
maximum_entries,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeList))?;
work = add(work, listing.work)?;
listing.work = work;
Ok(FsReceipt {
value: listing,
work,
})
}
pub async fn write_named_attribute(
&mut self,
name: AttributeName,
value: Bytes,
mode: NamedAttributeWriteMode,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileMetadata> {
let metadata = self.read_metadata(budget, cancellation).await?;
let mut work = metadata.work;
let mut metadata_value = metadata.value;
let root = match metadata_value.named_attributes {
MetadataField::Unavailable => {
let encoded = encode_attribute_page(
&AttributePage::Leaf(Vec::new()),
self.volume.config.limits.maximum_directory_page_entries,
)
.map_err(|error| OperationFailure::new(error.into(), work))?;
let stored = self
.volume
.fs
.put_encoded(
ObjectKind::AttributePage,
encoded,
work,
budget,
cancellation,
)
.await?;
work = stored.1;
stored.0
}
MetadataField::Value(root) => root,
};
let current = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
&name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, current.work)?;
match (mode, current.entry.as_ref()) {
(NamedAttributeWriteMode::Create, Some(_))
| (NamedAttributeWriteMode::Replace, None) => {
return Err(OperationFailure::new(FsError::CreationRejected, work));
}
_ => {}
}
let blob = self
.stage_blob(value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, blob.work)?;
let replacement = AttributeEntry {
name,
value_bytes: blob.value.logical_bytes,
value: blob.value.root,
};
let mutation = match current.entry {
None => AttributeMutation::Insert(replacement),
Some(expected) => AttributeMutation::Replace {
expected,
replacement,
},
};
let changed = apply_attribute_mutations_async(
&self.volume.fs.inner.objects,
root,
vec![mutation],
1,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeMutation))?;
work = add(work, changed.work)?;
metadata_value.named_attributes = MetadataField::Value(changed.root);
let metadata = self
.set_metadata(metadata_value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, metadata.work)?;
Ok(FsReceipt {
value: metadata_value,
work,
})
}
pub async fn remove_named_attribute(
&mut self,
name: AttributeName,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileMetadata> {
let metadata = self.read_metadata(budget, cancellation).await?;
let mut work = metadata.work;
let mut metadata_value = metadata.value;
let MetadataField::Value(root) = metadata_value.named_attributes else {
return Err(OperationFailure::new(FsError::NotFound, work));
};
let current = lookup_attribute_async(
&self.volume.fs.inner.objects,
root,
&name,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeLookup))?;
work = add(work, current.work)?;
let expected = current
.entry
.ok_or_else(|| OperationFailure::new(FsError::NotFound, work))?;
let changed = apply_attribute_mutations_async(
&self.volume.fs.inner.objects,
root,
vec![AttributeMutation::Remove {
name,
expected: Some(expected),
}],
1,
decode_limits(self.volume.config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, FsError::AttributeMutation))?;
work = add(work, changed.work)?;
metadata_value.named_attributes = MetadataField::Value(changed.root);
let metadata = self
.set_metadata(metadata_value, remaining(work, budget)?, cancellation)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, metadata.work)?;
Ok(FsReceipt {
value: metadata_value,
work,
})
}
pub async fn read_metadata(
&self,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileMetadata> {
let metadata = self
.volume
.fs
.inner
.objects
.read(
self.record.metadata,
self.volume.config.limits.maximum_object_bytes,
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(FsError::Object(failure.error), *failure.work)
})?;
let value = decode_file_metadata(&metadata.value, decode_limits(self.volume.config))
.map_err(|error| OperationFailure::new(error.into(), metadata.work))?;
Ok(FsReceipt {
value,
work: metadata.work,
})
}
async fn stage_blob(
&self,
bytes: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<crate::kernel::BlobBuild> {
if u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> self.volume.config.limits.maximum_read_bytes
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let maximum_blob_bytes = u64::try_from(bytes.len()).unwrap_or(u64::MAX).max(1);
let mut source = std::io::Cursor::new(bytes);
let staged = stage_content_for_volume(
&self.volume,
&mut source,
maximum_blob_bytes,
budget,
cancellation,
)
.await?;
Ok(FsReceipt {
work: staged.work,
value: crate::kernel::BlobBuild {
root: staged.value.root,
logical_bytes: staged.value.logical_bytes,
work: staged.work,
},
})
}
pub async fn read_range(
&self,
range: ByteRange,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<FileRangeRead> {
if range.length > self.volume.config.limits.maximum_read_bytes {
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let read = read_file_range_async(
&self.volume.fs.inner.objects,
FileRangeRequest {
record: self.record,
range,
maximum_spans: self.volume.config.limits.maximum_mutations_per_batch,
limits: decode_limits(self.volume.config),
budget,
},
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
Ok(FsReceipt {
work: read.work,
value: read,
})
}
pub async fn write_range(
&mut self,
offset: u64,
bytes: Bytes,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
if bytes.is_empty() {
return Ok(FsReceipt {
value: (),
work: WorkCounters::default(),
});
}
if u64::try_from(bytes.len()).unwrap_or(u64::MAX)
> self.volume.config.limits.maximum_read_bytes
{
return Err(OperationFailure::before_work(FsError::FileRead(
FileRangeReadError::InvalidRange,
)));
}
let maximum_blob_bytes = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
let mut source = std::io::Cursor::new(bytes);
let blob = stage_content_for_volume(
&self.volume,
&mut source,
maximum_blob_bytes,
budget,
cancellation,
)
.await?;
let mut work = blob.work;
let mutation = self
.mutate_regular(
RegularMutation::Write {
offset,
length: blob.value.logical_bytes,
content: blob.value.root,
content_offset: 0,
},
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(work, std::convert::identity))?;
work = add(work, mutation.work)?;
Ok(FsReceipt { value: (), work })
}
pub async fn resize(
&mut self,
logical_bytes: u64,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate_regular(
RegularMutation::Resize { logical_bytes },
budget,
cancellation,
)
.await
}
pub async fn zero_range(
&mut self,
range: ByteRange,
allocated: bool,
extend: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate_regular(
RegularMutation::ZeroRange {
offset: range.offset,
length: range.length,
allocated,
extend,
},
budget,
cancellation,
)
.await
}
pub async fn preallocate(
&mut self,
range: ByteRange,
keep_size: bool,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
self.mutate_regular(
RegularMutation::Preallocate {
offset: range.offset,
length: range.length,
keep_size,
},
budget,
cancellation,
)
.await
}
async fn mutate_regular(
&mut self,
mutation: RegularMutation,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<()> {
let receipt = apply_regular_mutation_async(
&self.volume.fs.inner.objects,
self.record.payload,
mutation,
self.volume.config,
budget,
cancellation,
)
.await
.map_err(|failure| {
OperationFailure::new(FsError::DetachedMutation(failure.error), *failure.work)
})?;
self.record.payload = receipt.payload;
Ok(FsReceipt {
value: (),
work: receipt.work,
})
}
}
pub(crate) async fn read_generation_root<O: AsyncObjectStore>(
objects: &O,
generation_root: ObjectId,
config: VolumeConfig,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> Result<(GenerationRoot, WorkCounters), OperationFailure<FsError>> {
cancellation
.check()
.map_err(|error| OperationFailure::before_work(error.into()))?;
let receipt = objects
.read(
generation_root,
MAXIMUM_GENERATION_ROOT_BYTES.min(config.limits.maximum_object_bytes),
budget,
cancellation,
)
.await
.map_err(|failure| OperationFailure::new(failure.error.into(), *failure.work))?;
let retained_bytes = match receipt.value.retention {
ObjectReadRetention::Shared => 0,
ObjectReadRetention::Owned { logical_bytes } => logical_bytes,
};
let decode = decode_limits(config);
let parent_count = generation_root_parent_count(&receipt.value, decode)
.map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
let parent_bytes = parent_count
.checked_mul(size_of::<GenerationId>())
.map(crate::foundation::usize_to_u64)
.ok_or_else(|| OperationFailure::new(FsError::Work(WorkError::Overflow), receipt.work))?;
let simultaneous = retained_bytes
.checked_add(parent_bytes)
.ok_or_else(|| OperationFailure::new(FsError::Work(WorkError::Overflow), receipt.work))?;
let mut work = receipt
.work
.checked_add(WorkCounters {
allocation_operations: u64::from(parent_count != 0),
bytes_copied: parent_bytes,
..WorkCounters::default()
})
.map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
work.peak_allocation_bytes = work.peak_allocation_bytes.max(simultaneous);
work.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), receipt.work))?;
let root = crate::kernel::decode_generation_root(&receipt.value, decode)
.map_err(|error| OperationFailure::new(error.into(), work))?;
Ok((root, work))
}
fn exact_mutation_regions(
mutation: &Mutation,
first: Option<FileRecord>,
second: Option<FileRecord>,
) -> [Option<(Option<FileRecord>, DependencyRegion)>; 4] {
let mut regions = [None, None, None, None];
match mutation {
Mutation::Create { .. } | Mutation::Link { .. } => {}
Mutation::Restore { .. } => {
regions[0] =
first.map(|record| (Some(record), DependencyRegion::FileRecord(record.file_id)));
}
Mutation::Remove { .. } => {
regions[0] =
first.map(|record| (Some(record), DependencyRegion::FileRecord(record.file_id)));
}
Mutation::Rename { .. } => {
regions[0] =
second.map(|record| (Some(record), DependencyRegion::FileRecord(record.file_id)));
}
Mutation::SetMetadata { .. } => {
regions[0] =
first.map(|record| (Some(record), DependencyRegion::Metadata(record.file_id)));
}
Mutation::Write { offset, length, .. } | Mutation::ZeroRange { offset, length, .. } => {
let [content, file_length] = regular_range_regions(first, *offset, *length);
regions[0] = content.map(|region| (first, region));
regions[1] = file_length.map(|region| (first, region));
}
Mutation::ValidateRegular { .. } => {
regions[0] =
first.map(|record| (Some(record), DependencyRegion::FileRecord(record.file_id)));
}
Mutation::Preallocate { .. } => {
regions[0] =
first.map(|record| (Some(record), DependencyRegion::FileLength(record.file_id)));
}
Mutation::Resize { .. } => {
regions[0] =
first.map(|record| (Some(record), DependencyRegion::FileRecord(record.file_id)));
}
Mutation::CloneRange {
source_offset,
destination_offset,
length,
..
} => {
let [source_content, source_length] =
regular_range_regions(first, *source_offset, *length);
let [destination_content, destination_length] =
regular_range_regions(second, *destination_offset, *length);
regions[0] = source_content.map(|region| (first, region));
regions[1] = source_length.map(|region| (first, region));
regions[2] = destination_content.map(|region| (second, region));
regions[3] = destination_length.map(|region| (second, region));
}
Mutation::File { file_id, mutation } => match mutation {
FileMutation::SetMetadata { .. } => {
regions[0] = Some((first, DependencyRegion::Metadata(*file_id)));
}
FileMutation::Write { offset, length, .. }
| FileMutation::ZeroRange { offset, length, .. } => {
let [content, file_length] = regular_range_regions(first, *offset, *length);
regions[0] = content.map(|region| (first, region));
regions[1] = file_length.map(|region| (first, region));
}
FileMutation::Preallocate { .. } => {
regions[0] = Some((first, DependencyRegion::FileLength(*file_id)));
}
FileMutation::ReplaceRecord { .. }
| FileMutation::ValidateRegular
| FileMutation::Resize { .. } => {
regions[0] = Some((first, DependencyRegion::FileRecord(*file_id)));
}
},
Mutation::CloneFileRange {
source_file_id,
source_offset,
destination_file_id,
destination_offset,
length,
} => {
let [source_content, source_length] =
regular_range_regions(first, *source_offset, *length);
let [destination_content, destination_length] =
regular_range_regions(second, *destination_offset, *length);
regions[0] = source_content.map(|region| (first, region));
regions[1] = source_length.map(|region| (first, region));
regions[2] = destination_content.map(|region| (second, region));
regions[3] = destination_length.map(|region| (second, region));
debug_assert!(first.is_none_or(|record| record.file_id == *source_file_id));
debug_assert!(second.is_none_or(|record| record.file_id == *destination_file_id));
}
}
regions
}
fn regular_range_regions(
record: Option<FileRecord>,
offset: u64,
length: u64,
) -> [Option<DependencyRegion>; 2] {
let Some(record) = record else {
return [None, None];
};
let logical_bytes = match record.payload {
FilePayload::InlineRegular(data) => {
u64::try_from(data.as_bytes().len()).unwrap_or(u64::MAX)
}
FilePayload::Regular { logical_bytes, .. } => logical_bytes,
FilePayload::Directory { .. }
| FilePayload::SymbolicLink { .. }
| FilePayload::Device { .. }
| FilePayload::Empty
| FilePayload::ReparsePoint { .. } => {
return [Some(DependencyRegion::FileRecord(record.file_id)), None];
}
};
let overlap = logical_bytes.saturating_sub(offset).min(length);
let content = (overlap != 0).then_some(DependencyRegion::ContentRange {
file_id: record.file_id,
offset,
length: overlap,
});
let extends = offset
.checked_add(length)
.is_none_or(|end| end > logical_bytes);
let file_length = extends.then_some(DependencyRegion::FileLength(record.file_id));
[content, file_length]
}
fn validate_planned_file_range(
logical_bytes: u64,
range: ByteRange,
work: WorkCounters,
) -> Result<(), OperationFailure<FsError>> {
let end = range.offset.checked_add(range.length).ok_or_else(|| {
OperationFailure::new(FsError::FileRead(FileRangeReadError::InvalidRange), work)
})?;
if end > logical_bytes {
return Err(OperationFailure::new(
FsError::FileRead(FileRangeReadError::InvalidRange),
work,
));
}
Ok(())
}
async fn capture_dependencies_async<O: AsyncObjectStore>(
objects: &O,
generation_root: &ObjectId,
config: VolumeConfig,
regions: Vec<DependencyRegion>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<Dependency>> {
let probe = AuthenticatedGenerationProbe::new(objects, probe_limits(config))
.map_err(|error| OperationFailure::before_work(error.into()))?;
probe
.capture_many_async(
GenerationId::new(generation_root.digest),
regions,
config.limits.maximum_checkout_dependencies,
budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(WorkCounters::default(), FsError::Probe))
}
async fn capture_known_dependencies_async<O: AsyncObjectStore>(
objects: &O,
config: VolumeConfig,
regions: Vec<(Option<FileRecord>, DependencyRegion)>,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<Vec<Dependency>> {
let probe = AuthenticatedGenerationProbe::new(objects, probe_limits(config))
.map_err(|error| OperationFailure::before_work(error.into()))?;
probe
.capture_records_many_async(
regions,
config.limits.maximum_checkout_dependencies,
budget,
cancellation,
)
.await
.map_err(|failure| failure.map_with_prior_work(WorkCounters::default(), FsError::Probe))
}
fn generation_from_record(
record: &DurableCommit,
volume_id: VolumeId,
work: WorkCounters,
) -> Result<ObjectId, OperationFailure<FsError>> {
if record.sequence == Sequence::new(1) {
let creation = decode_volume_created(&record.payload, MAXIMUM_VOLUME_EVENT_BYTES)
.map_err(|error| OperationFailure::new(error.into(), work))?;
if creation.volume_id != volume_id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
Ok(creation.initial_generation_root)
} else {
let publication = decode_published_generation(&record.payload, MAXIMUM_VOLUME_EVENT_BYTES)
.map_err(|error| OperationFailure::new(error.into(), work))?;
if publication.volume_id != volume_id {
return Err(OperationFailure::new(FsError::VolumeMismatch, work));
}
Ok(publication.generation_root)
}
}
#[allow(clippy::too_many_arguments)]
async fn diff_generation_file_tables<O: AsyncObjectStore>(
objects: &O,
before: ObjectId,
after: ObjectId,
maximum_changes: u32,
config: VolumeConfig,
mut work: WorkCounters,
budget: WorkBudget,
cancellation: &CancellationToken,
) -> FsResult<GenerationDiff> {
let records = diff_file_records_async(
objects,
Some(before),
Some(after),
maximum_changes,
decode_limits(config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| map_diff_failure(failure, work))?;
work = add(work, records.work)?;
let maximum = usize::try_from(maximum_changes).unwrap_or(usize::MAX);
let mut files = Vec::new();
files
.try_reserve(records.changes.len())
.map_err(|_| OperationFailure::new(FsError::DiffAllocationFailed, work))?;
let mut bindings = Vec::new();
let mut truncated = records.truncated;
for change in records.changes {
if files.len() + bindings.len() >= maximum {
truncated = true;
break;
}
let before_entries = directory_entries(change.before);
let after_entries = directory_entries(change.after);
files.push(FileRecordChange {
file_id: change.key,
before: change.before,
after: change.after,
});
if before_entries == after_entries {
continue;
}
let remaining_changes = maximum.saturating_sub(files.len() + bindings.len());
if remaining_changes == 0 {
truncated = true;
continue;
}
let entries = diff_tree_entries_async(
objects,
before_entries,
after_entries,
u32::try_from(remaining_changes).unwrap_or(u32::MAX),
decode_limits(config),
remaining(work, budget)?,
cancellation,
)
.await
.map_err(|failure| map_diff_failure(failure, work))?;
work = add(work, entries.work)?;
truncated |= entries.truncated;
bindings.extend(
entries
.changes
.into_iter()
.map(|entry| DirectoryBindingChange {
directory_id: change.key,
name: entry.key,
before: entry.before,
after: entry.after,
}),
);
}
Ok(OperationReceipt {
value: GenerationDiff {
files,
bindings,
truncated,
},
work,
})
}
async fn collect_generation_ancestors<O: AsyncObjectStore>(
objects: &O,
start: GenerationId,
config: VolumeConfig,
maximum_generations: u32,
cancellation: &CancellationToken,
) -> Result<(BTreeMap<GenerationId, Option<VolumeId>>, bool), crate::workspace::WorkspaceError> {
let maximum = usize::try_from(maximum_generations).unwrap_or(usize::MAX);
let mut visited = BTreeMap::new();
let mut pending = VecDeque::from([start]);
while let Some(generation) = pending.pop_front() {
if visited.contains_key(&generation) {
continue;
}
if visited.len() == maximum {
for frontier in pending {
visited.entry(frontier).or_insert(None);
}
visited.entry(generation).or_insert(None);
return Ok((visited, true));
}
let (root, _) = read_generation_root(
objects,
ObjectId {
kind: ObjectKind::GenerationRoot,
digest: generation.digest(),
},
config,
WorkBudget::UNBOUNDED,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
visited.insert(generation, Some(root.volume_id));
pending.extend(root.parents);
}
Ok((visited, false))
}
async fn find_first_generation_ancestor<O: AsyncObjectStore>(
objects: &O,
start: GenerationId,
config: VolumeConfig,
maximum_generations: u32,
candidates: &BTreeMap<GenerationId, Option<VolumeId>>,
candidates_truncated: bool,
cancellation: &CancellationToken,
) -> Result<WorkspaceCommonAncestor, crate::workspace::WorkspaceError> {
let maximum = usize::try_from(maximum_generations).unwrap_or(usize::MAX);
let mut visited = BTreeSet::new();
let mut pending = VecDeque::from([start]);
while let Some(generation) = pending.pop_front() {
if let Some(Some(volume_id)) = candidates.get(&generation) {
return Ok(WorkspaceCommonAncestor {
id: generation,
volume_id: *volume_id,
});
}
let is_candidate_frontier = matches!(candidates.get(&generation), Some(None));
if visited.contains(&generation) {
continue;
}
if visited.len() == maximum {
return Err(crate::workspace::WorkspaceError::LineageLimit);
}
let (root, _) = read_generation_root(
objects,
ObjectId {
kind: ObjectKind::GenerationRoot,
digest: generation.digest(),
},
config,
WorkBudget::UNBOUNDED,
cancellation,
)
.await
.map_err(crate::workspace::WorkspaceError::engine)?;
visited.insert(generation);
if is_candidate_frontier {
return Ok(WorkspaceCommonAncestor {
id: generation,
volume_id: root.volume_id,
});
}
pending.extend(root.parents);
}
if candidates_truncated {
Err(crate::workspace::WorkspaceError::LineageLimit)
} else {
Err(crate::workspace::WorkspaceError::NoCommonAncestor)
}
}
fn durable_head(commit: &DurableCommit) -> Head {
Head {
epoch: commit.epoch,
sequence: commit.sequence,
digest: commit.digest,
}
}
fn validate_volume_capabilities(
config: VolumeConfig,
capabilities: EmbeddedCapabilities,
) -> Result<(), OperationFailure<FsError>> {
config
.validate()
.map_err(|error| OperationFailure::before_work(error.into()))?;
if config.lifecycle == Lifecycle::Durable && !capabilities.durable {
return Err(OperationFailure::before_work(
FsError::UnsupportedDurability,
));
}
Ok(())
}
fn validate_checkout(
mode: CheckoutMode,
config: VolumeConfig,
) -> Result<(), OperationFailure<FsError>> {
mode.validate()
.map_err(|error| OperationFailure::before_work(error.into()))?;
match (mode.access, mode.consistency, mode.mutations) {
(
AccessMode::ReadOnly,
ConsistencyMode::Pinned
| ConsistencyMode::Manual
| ConsistencyMode::TrackingSafe
| ConsistencyMode::Live,
MutationMode::None,
)
| (
AccessMode::ReadWrite,
ConsistencyMode::Pinned
| ConsistencyMode::Manual
| ConsistencyMode::TrackingSafe
| ConsistencyMode::Live,
MutationMode::PrivateOverlay,
) => Ok(()),
(AccessMode::ReadWrite, ConsistencyMode::Live, MutationMode::DirectLive)
if config.concurrency == ConcurrencyMode::SerializedAuthority =>
{
Ok(())
}
(AccessMode::ReadWrite, ConsistencyMode::Live, MutationMode::DirectLive) => Err(
OperationFailure::before_work(FsError::LiveRequiresSerializedAuthority),
),
_ => Err(OperationFailure::before_work(FsError::UnsupportedCheckout)),
}
}
fn closure_limits(config: VolumeConfig) -> ClosureLimits {
ClosureLimits {
decode: decode_limits(config),
maximum_objects: config.limits.maximum_objects_per_generation,
maximum_files: config.limits.maximum_files_per_generation,
maximum_object_bytes: config.limits.maximum_generation_bytes,
profile: config.profile,
symbolic_links: config.symbolic_links,
hard_links: config.hard_links,
sparse_files: config.sparse_files,
}
}
fn decode_limits(config: VolumeConfig) -> DecodeLimits {
DecodeLimits {
maximum_object_bytes: config.limits.maximum_object_bytes,
maximum_name_bytes: config.limits.maximum_component_bytes,
maximum_page_items: config.limits.maximum_directory_page_entries,
maximum_page_bytes: u32::try_from(config.limits.maximum_object_bytes).unwrap_or(u32::MAX),
maximum_page_height: config.limits.maximum_page_height,
maximum_visited_pages: u32::try_from(config.limits.maximum_objects_per_generation)
.unwrap_or(u32::MAX),
}
}
fn directory_entries(record: Option<FileRecord>) -> Option<ObjectId> {
let record = record?;
match record.payload {
FilePayload::Directory { entries } => Some(entries),
_ => None,
}
}
fn map_diff_failure(
failure: OperationFailure<PersistentDiffError>,
prior: WorkCounters,
) -> OperationFailure<FsError> {
failure.map_with_prior_work(prior, |error| match error {
PersistentDiffError::WrongRootKind | PersistentDiffError::InvalidLimit => {
FsError::InvalidDiff
}
PersistentDiffError::AllocationFailed => FsError::DiffAllocationFailed,
PersistentDiffError::Storage(error) => FsError::Object(error),
PersistentDiffError::Decode(error) => FsError::Decode(error),
PersistentDiffError::Work(error) => FsError::Work(error),
PersistentDiffError::Cancelled => FsError::Cancelled(CancellationError),
})
}
fn map_merge_failure(
failure: OperationFailure<MergeGenerationError>,
prior: WorkCounters,
) -> OperationFailure<FsError> {
failure.map_with_prior_work(prior, |error| match error {
MergeGenerationError::InvalidDiff => FsError::InvalidDiff,
MergeGenerationError::ChangeLimit => FsError::MergeChangeLimit,
MergeGenerationError::AllocationFailed => FsError::DiffAllocationFailed,
MergeGenerationError::Object(error) => FsError::Object(error),
MergeGenerationError::Decode(error) => FsError::Decode(error),
MergeGenerationError::FileTable(error) => FsError::MergeFileTable(error),
MergeGenerationError::Tree(error) => FsError::MergeTree(error),
MergeGenerationError::ExtentRead(error) => {
FsError::FileRead(FileRangeReadError::Extent(error))
}
MergeGenerationError::ExtentMutation(error) => {
FsError::DetachedMutation(RegularMutationError::Extent(error))
}
MergeGenerationError::Checkpoint(error) => FsError::Checkpoint(error),
MergeGenerationError::Cancelled(error) => FsError::Cancelled(error),
MergeGenerationError::Work(error) => FsError::Work(error),
})
}
const fn live_publication_observation(
outcome: CheckoutCommitOutcome,
) -> LivePublicationObservation {
match outcome {
CheckoutCommitOutcome::Committed {
generation_id,
head,
} => LivePublicationObservation::Committed {
generation_id,
head,
},
CheckoutCommitOutcome::AlreadyCommitted {
generation_id,
head,
} => LivePublicationObservation::AlreadyCommitted {
generation_id,
head,
},
CheckoutCommitOutcome::Conflict { actual } => {
LivePublicationObservation::Conflict { actual }
}
CheckoutCommitOutcome::Fenced { actual_epoch } => {
LivePublicationObservation::Fenced { actual_epoch }
}
CheckoutCommitOutcome::IdempotencyConflict {
committed_fingerprint,
} => LivePublicationObservation::IdempotencyConflict {
committed_fingerprint,
},
}
}
fn probe_limits(config: VolumeConfig) -> ProbeLimits {
ProbeLimits {
decode: decode_limits(config),
maximum_cached_generations: 2,
maximum_cached_records: config.limits.maximum_checkout_dependencies,
maximum_extent_spans: config.limits.maximum_directory_page_entries,
maximum_content_payload_bytes: config.limits.maximum_read_bytes,
maximum_directory_entries: config.limits.maximum_directory_page_entries,
}
}
fn empty_metadata() -> FileMetadata {
FileMetadata::default()
}
fn derived_file_id(volume_id: VolumeId) -> (FileId, WorkCounters) {
let digest = derived_digest(ROOT_FILE_DOMAIN, volume_id);
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest.as_bytes()[..16]);
(
FileId::from_bytes(bytes),
identity_hash_work(ROOT_FILE_DOMAIN),
)
}
fn derived_operation_id(volume_id: VolumeId) -> (OperationId, WorkCounters) {
let digest = derived_digest(CREATION_OPERATION_DOMAIN, volume_id);
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest.as_bytes()[..16]);
(
OperationId::from_bytes(bytes),
identity_hash_work(CREATION_OPERATION_DOMAIN),
)
}
fn derived_digest(domain: &[u8], volume_id: VolumeId) -> Digest {
let mut hasher = blake3::Hasher::new();
hasher.update(domain);
hasher.update(&volume_id.into_bytes());
Digest::from_bytes(*hasher.finalize().as_bytes())
}
fn identity_hash_work(domain: &[u8]) -> WorkCounters {
WorkCounters {
bytes_hashed: u64::try_from(domain.len()).unwrap_or(u64::MAX) + 16,
..WorkCounters::default()
}
}
fn creation_commit(operation_id: OperationId, payload: Vec<u8>) -> (ProposedCommit, WorkCounters) {
let mut hasher = blake3::Hasher::new();
hasher.update(CREATION_FINGERPRINT_DOMAIN);
hasher.update(&payload);
let length = u64::try_from(payload.len()).unwrap_or(u64::MAX);
let capacity = u64::try_from(payload.capacity()).unwrap_or(u64::MAX);
(
ProposedCommit {
operation_id,
fingerprint: Digest::from_bytes(*hasher.finalize().as_bytes()),
payload: Bytes::from(payload),
},
WorkCounters {
bytes_encoded: length,
bytes_hashed: u64::try_from(CREATION_FINGERPRINT_DOMAIN.len())
.unwrap_or(u64::MAX)
.saturating_add(length),
allocation_operations: 1,
peak_allocation_bytes: capacity,
..WorkCounters::default()
},
)
}
fn add(prior: WorkCounters, next: WorkCounters) -> Result<WorkCounters, OperationFailure<FsError>> {
prior
.checked_add(next)
.map_err(|error| OperationFailure::new(error.into(), prior))
}
fn map_transfer_error(error: GenerationTransferError) -> FsError {
match error {
GenerationTransferError::Cancelled(error) => FsError::Cancelled(error),
GenerationTransferError::Manifest(_) | GenerationTransferError::ManifestMismatch => {
FsError::InvalidExportManifest
}
GenerationTransferError::Closure(error) => FsError::Closure(error),
GenerationTransferError::Object(error) => FsError::Object(error),
GenerationTransferError::Work(error) => FsError::Work(error),
error @ (GenerationTransferError::InvalidCursor
| GenerationTransferError::EmptyBatch
| GenerationTransferError::TooManyObjects
| GenerationTransferError::AllocationFailed) => FsError::Transfer(error),
}
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
fn object_vec_bytes(objects: &Vec<ObjectId>) -> u64 {
u64::try_from(objects.capacity())
.unwrap_or(u64::MAX)
.saturating_mul(u64::try_from(size_of::<ObjectId>()).unwrap_or(u64::MAX))
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
fn object_slice_bytes(objects: &[ObjectId]) -> u64 {
u64::try_from(objects.len())
.unwrap_or(u64::MAX)
.saturating_mul(u64::try_from(size_of::<ObjectId>()).unwrap_or(u64::MAX))
}
#[cfg(all(feature = "local", not(target_arch = "wasm32")))]
fn merge_sorted_object_ids(
reachable: &mut Vec<ObjectId>,
incoming: &[ObjectId],
incoming_bytes: u64,
retained_bytes: u64,
work: &mut WorkCounters,
budget: WorkBudget,
) -> Result<(), OperationFailure<FsError>> {
let maximum_items = reachable
.len()
.checked_add(incoming.len())
.ok_or_else(|| OperationFailure::new(FsError::Work(WorkError::Overflow), *work))?;
let maximum_bytes = u64::try_from(maximum_items)
.unwrap_or(u64::MAX)
.saturating_mul(u64::try_from(size_of::<ObjectId>()).unwrap_or(u64::MAX));
let peak = retained_bytes
.checked_add(object_vec_bytes(reachable))
.and_then(|value| value.checked_add(incoming_bytes))
.and_then(|value| value.checked_add(maximum_bytes))
.ok_or_else(|| OperationFailure::new(FsError::Work(WorkError::Overflow), *work))?;
let admission = work
.checked_add(WorkCounters {
items_examined: u64::try_from(maximum_items).unwrap_or(u64::MAX),
bytes_copied: maximum_bytes,
allocation_operations: u64::from(maximum_items != 0),
peak_allocation_bytes: peak,
..WorkCounters::default()
})
.map_err(|error| OperationFailure::new(error.into(), *work))?;
admission
.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), *work))?;
let mut merged = Vec::new();
merged
.try_reserve_exact(maximum_items)
.map_err(|_| OperationFailure::new(FsError::GarbageCollectionAllocationFailed, *work))?;
let mut left_iter = reachable.iter().copied().peekable();
let mut right_iter = incoming.iter().copied().peekable();
loop {
match (left_iter.peek(), right_iter.peek()) {
(Some(&left), Some(&right)) => match left.cmp(&right) {
std::cmp::Ordering::Less => {
merged.push(left);
left_iter.next();
}
std::cmp::Ordering::Greater => {
merged.push(right);
right_iter.next();
}
std::cmp::Ordering::Equal => {
merged.push(left);
left_iter.next();
right_iter.next();
}
},
(Some(&left), None) => {
merged.push(left);
left_iter.next();
}
(None, Some(&right)) => {
merged.push(right);
right_iter.next();
}
(None, None) => break,
}
}
let copied_bytes = u64::try_from(merged.len())
.unwrap_or(u64::MAX)
.saturating_mul(u64::try_from(size_of::<ObjectId>()).unwrap_or(u64::MAX));
*work = work
.checked_add(WorkCounters {
items_examined: u64::try_from(maximum_items).unwrap_or(u64::MAX),
bytes_copied: copied_bytes,
allocation_operations: u64::from(maximum_items != 0),
peak_allocation_bytes: peak,
..WorkCounters::default()
})
.map_err(|error| OperationFailure::new(error.into(), *work))?;
*reachable = merged;
Ok(())
}
fn remaining(
work: WorkCounters,
budget: WorkBudget,
) -> Result<WorkBudget, OperationFailure<FsError>> {
work.remaining(budget)
.map_err(|error| OperationFailure::new(error.into(), work))
}
fn merge_simultaneous_work(
prior: WorkCounters,
mut nested: WorkCounters,
live_bytes: u64,
budget: WorkBudget,
) -> Result<WorkCounters, OperationFailure<FsError>> {
let simultaneous_peak = live_bytes
.checked_add(nested.peak_allocation_bytes)
.ok_or_else(|| OperationFailure::new(FsError::Work(WorkError::Overflow), prior))?;
nested.peak_allocation_bytes = 0;
let mut merged = prior
.checked_add(nested)
.map_err(|error| OperationFailure::new(error.into(), prior))?;
merged.peak_allocation_bytes = merged.peak_allocation_bytes.max(simultaneous_peak);
merged
.verify(budget)
.map_err(|error| OperationFailure::new(error.into(), merged))?;
Ok(merged)
}
fn merge_simultaneous_failure(
prior: WorkCounters,
mut nested: WorkCounters,
live_bytes: u64,
error: FsError,
) -> OperationFailure<FsError> {
let Some(simultaneous_peak) = live_bytes.checked_add(nested.peak_allocation_bytes) else {
return OperationFailure::new(FsError::Work(WorkError::Overflow), prior);
};
nested.peak_allocation_bytes = 0;
let Ok(mut merged) = prior.checked_add(nested) else {
return OperationFailure::new(FsError::Work(WorkError::Overflow), prior);
};
merged.peak_allocation_bytes = merged.peak_allocation_bytes.max(simultaneous_peak);
OperationFailure::new(error, merged)
}
#[cfg(all(test, feature = "memory"))]
#[path = "tests/facade.rs"]
mod tests;