1use crate::demand::{
8 DemandError, DemandSource, SourceCursor, SourceDirectoryEntry, SourceNode, SourceNodeKind,
9 SourceReference, SourceVersion,
10};
11use crate::kernel::{
12 FileKind, FileMetadata, FilePayload, FileRecord, LogicalName, MetadataField, NameEncoding,
13 NamespacePath,
14};
15use crate::model::VolumeConfig;
16use crate::path::PortablePath;
17use crate::{
18 AsyncAuthorityStore, AsyncObjectStore, AuthoredMutation, CancellationToken, FileId,
19 ForkOptions, Fs, IdempotencyKey, OperationReceipt, TransactionCommit, WorkBudget, WorkCounters,
20 Workspace, WorkspaceDirectoryEntry, WorkspaceError, WorkspaceExtentKind, WorkspaceId,
21 WorkspaceStat,
22};
23use async_trait::async_trait;
24use bytes::Bytes;
25use serde::{Deserialize, Serialize};
26use std::collections::BTreeMap;
27use std::convert::Infallible;
28use std::future::Future;
29use std::pin::Pin;
30use std::sync::{Arc, Mutex};
31use thiserror::Error;
32
33#[cfg(target_arch = "wasm32")]
34type LazyFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
35#[cfg(not(target_arch = "wasm32"))]
36type LazyFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
37
38const LAZY_STATE_SCHEMA: u32 = 7;
39const MAXIMUM_STATE_RETRIES: usize = 32;
40
41const LAZY_SNAPSHOT_DOMAIN: &[u8] = b"acyclic-fs-lazy-snapshot-v1\0";
42
43#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
45#[serde(transparent)]
46pub struct LazySnapshotId([u8; 32]);
47
48impl LazySnapshotId {
49 #[must_use]
51 pub const fn into_bytes(self) -> [u8; 32] {
52 self.0
53 }
54}
55
56#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
58pub struct LazySnapshotRef {
59 pub id: LazySnapshotId,
61 pub workspace_id: WorkspaceId,
63 pub authored_generation: crate::GenerationId,
65 pub source: SourceReference,
67 pub overlay: LazyOverlayId,
69 pub shadows: LazyShadowId,
71}
72
73impl LazySnapshotRef {
74 fn new(
75 workspace_id: WorkspaceId,
76 authored_generation: crate::GenerationId,
77 source: SourceReference,
78 overlay: LazyOverlayId,
79 shadows: LazyShadowId,
80 ) -> Self {
81 let mut hasher = blake3::Hasher::new();
82 hasher.update(LAZY_SNAPSHOT_DOMAIN);
83 hasher.update(&workspace_id.into_bytes());
84 hasher.update(authored_generation.digest().as_bytes());
85 hasher.update(&source.identity);
86 hasher.update(&source.epoch.to_le_bytes());
87 hasher.update(&overlay.into_bytes());
88 hasher.update(&shadows.into_bytes());
89 Self {
90 id: LazySnapshotId(*hasher.finalize().as_bytes()),
91 workspace_id,
92 authored_generation,
93 source,
94 overlay,
95 shadows,
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
102#[serde(transparent)]
103pub struct LazyOverlayId([u8; 32]);
104
105impl LazyOverlayId {
106 #[must_use]
108 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
109 Self(bytes)
110 }
111
112 #[must_use]
114 pub const fn into_bytes(self) -> [u8; 32] {
115 self.0
116 }
117}
118
119#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
121#[serde(try_from = "LazyWorkspaceStateRecord")]
122pub struct LazyWorkspaceState {
123 pub schema_version: u32,
125 pub revision: u64,
127 pub workspace_id: WorkspaceId,
129 pub parent_workspace_id: Option<WorkspaceId>,
131 pub source: SourceReference,
133 pub overlay: LazyOverlayId,
135 pub shadows: LazyShadowId,
137 pub pending_remove: Option<PendingLazyRemove>,
139}
140
141#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
143#[serde(transparent)]
144pub struct LazyShadowId([u8; 32]);
145
146impl LazyShadowId {
147 #[must_use]
149 pub const fn into_bytes(self) -> [u8; 32] {
150 self.0
151 }
152}
153
154#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
156pub enum LazyShadow {
157 #[default]
159 Empty,
160 Node {
162 file_id: FileId,
164 priority: u64,
166 record: Vec<u8>,
168 metadata: Box<crate::WorkspaceMetadata>,
170 left: LazyShadowId,
172 right: LazyShadowId,
174 },
175}
176
177impl LazyShadow {
178 fn id(&self) -> Result<LazyShadowId, LazyWorkspaceError> {
179 let encoded = serde_json::to_vec(self)
180 .map_err(|error| LazyWorkspaceError::Store(error.to_string()))?;
181 Ok(LazyShadowId(*blake3::hash(&encoded).as_bytes()))
182 }
183}
184
185#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
187pub struct PendingLazyRemove {
188 pub prior_overlay: LazyOverlayId,
190 pub prior_shadows: LazyShadowId,
192 pub prepared_overlay: LazyOverlayId,
194 pub kind: PendingLazyRemoveKind,
196}
197
198#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
200pub enum PendingLazyRemoveKind {
201 SourceOnly,
203 Authored {
205 idempotency_key: IdempotencyKey,
207 },
208 Mounted {
210 idempotency_key: IdempotencyKey,
212 },
213}
214
215const LAZY_STATE_SCHEMA_V5: u32 = 5;
217
218#[derive(Deserialize)]
219struct LazyWorkspaceStateRecord {
220 schema_version: u32,
221 revision: u64,
222 workspace_id: WorkspaceId,
223 parent_workspace_id: Option<WorkspaceId>,
224 source: SourceReference,
225 overlay: LazyOverlayId,
226 shadows: LazyShadowId,
227 pending_remove: Option<PendingLazyRemoveRecord>,
228}
229
230#[derive(Deserialize)]
231#[serde(untagged)]
232enum PendingLazyRemoveRecord {
233 Current(PendingLazyRemove),
234 V5 {
235 prior_overlay: LazyOverlayId,
236 tombstone_overlay: LazyOverlayId,
237 kind: PendingLazyRemoveKind,
238 },
239}
240
241impl TryFrom<LazyWorkspaceStateRecord> for LazyWorkspaceState {
242 type Error = &'static str;
243
244 fn try_from(record: LazyWorkspaceStateRecord) -> Result<Self, Self::Error> {
245 let (schema_version, pending_remove) = match (record.schema_version, record.pending_remove)
246 {
247 (LAZY_STATE_SCHEMA_V5, None) => (LAZY_STATE_SCHEMA, None),
248 (
251 LAZY_STATE_SCHEMA_V5,
252 Some(PendingLazyRemoveRecord::V5 {
253 prior_overlay,
254 tombstone_overlay,
255 kind,
256 }),
257 ) => (
258 LAZY_STATE_SCHEMA,
259 Some(PendingLazyRemove {
260 prior_overlay,
261 prior_shadows: record.shadows,
262 prepared_overlay: tombstone_overlay,
263 kind,
264 }),
265 ),
266 (LAZY_STATE_SCHEMA_V5, Some(PendingLazyRemoveRecord::Current(_))) => {
267 return Err("schema 5 lazy state has a current pending removal");
268 }
269 (schema, None) => (schema, None),
270 (schema, Some(PendingLazyRemoveRecord::Current(pending))) => (schema, Some(pending)),
271 (_, Some(PendingLazyRemoveRecord::V5 { .. })) => {
272 return Err("lazy state has a schema 5 pending removal");
273 }
274 };
275 Ok(Self {
276 schema_version,
277 revision: record.revision,
278 workspace_id: record.workspace_id,
279 parent_workspace_id: record.parent_workspace_id,
280 source: record.source,
281 overlay: record.overlay,
282 shadows: record.shadows,
283 pending_remove,
284 })
285 }
286}
287
288#[allow(clippy::large_enum_variant)]
293#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
294pub enum LazyOverlay {
295 #[default]
297 Empty,
298 Node {
300 path: String,
302 priority: u64,
304 change: LazyOverlayChange,
306 left: LazyOverlayId,
308 right: LazyOverlayId,
310 },
311}
312
313#[allow(clippy::large_enum_variant)]
315#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
316pub enum LazyOverlayChange {
317 Observe {
319 source: SourceReference,
321 node: SourceNode,
323 },
324 Tombstone,
326}
327
328impl LazyOverlay {
329 fn id(&self) -> Result<LazyOverlayId, LazyWorkspaceError> {
330 let encoded = serde_json::to_vec(self)
331 .map_err(|error| LazyWorkspaceError::Store(error.to_string()))?;
332 Ok(LazyOverlayId(*blake3::hash(&encoded).as_bytes()))
333 }
334}
335
336#[async_trait]
338pub trait LazyWorkspaceStore: Clone + Send + Sync + 'static {
339 type Error: std::error::Error + Send + Sync + 'static;
341
342 async fn load_lazy_workspace(
344 &self,
345 workspace_id: WorkspaceId,
346 ) -> Result<Option<LazyWorkspaceState>, Self::Error>;
347
348 async fn load_lazy_workspace_measured(
350 &self,
351 workspace_id: WorkspaceId,
352 budget: WorkBudget,
353 cancellation: &CancellationToken,
354 ) -> Result<OperationReceipt<Option<LazyWorkspaceState>>, LazyWorkspaceError> {
355 measured_lazy_store_read(budget, cancellation)?;
356 let value = self
357 .load_lazy_workspace(workspace_id)
358 .await
359 .map_err(store_error)?;
360 cancellation
361 .check()
362 .map_err(|_| LazyWorkspaceError::Cancelled)?;
363 Ok(OperationReceipt {
364 value,
365 work: lazy_store_read_work(),
366 })
367 }
368
369 async fn compare_and_swap_lazy_workspace(
371 &self,
372 workspace_id: WorkspaceId,
373 expected_revision: u64,
374 replacement: LazyWorkspaceState,
375 ) -> Result<bool, Self::Error>;
376
377 async fn compare_and_swap_lazy_workspace_measured(
379 &self,
380 workspace_id: WorkspaceId,
381 expected_revision: u64,
382 replacement: LazyWorkspaceState,
383 budget: WorkBudget,
384 cancellation: &CancellationToken,
385 ) -> Result<OperationReceipt<bool>, LazyWorkspaceError> {
386 measured_lazy_store_write(budget, cancellation)?;
387 let value = self
388 .compare_and_swap_lazy_workspace(workspace_id, expected_revision, replacement)
389 .await
390 .map_err(store_error)?;
391 cancellation
392 .check()
393 .map_err(|_| LazyWorkspaceError::Cancelled)?;
394 Ok(OperationReceipt {
395 value,
396 work: lazy_store_write_work(),
397 })
398 }
399
400 async fn load_lazy_overlay(
402 &self,
403 overlay: LazyOverlayId,
404 ) -> Result<Option<LazyOverlay>, Self::Error>;
405
406 async fn load_lazy_overlay_measured(
408 &self,
409 overlay: LazyOverlayId,
410 budget: WorkBudget,
411 cancellation: &CancellationToken,
412 ) -> Result<OperationReceipt<Option<LazyOverlay>>, LazyWorkspaceError> {
413 measured_lazy_store_read(budget, cancellation)?;
414 let value = self.load_lazy_overlay(overlay).await.map_err(store_error)?;
415 cancellation
416 .check()
417 .map_err(|_| LazyWorkspaceError::Cancelled)?;
418 Ok(OperationReceipt {
419 value,
420 work: lazy_store_read_work(),
421 })
422 }
423
424 async fn put_lazy_overlay(
426 &self,
427 overlay: LazyOverlayId,
428 value: LazyOverlay,
429 ) -> Result<(), Self::Error>;
430
431 async fn load_lazy_shadow(
433 &self,
434 shadow: LazyShadowId,
435 ) -> Result<Option<LazyShadow>, Self::Error>;
436
437 async fn load_lazy_shadow_measured(
439 &self,
440 shadow: LazyShadowId,
441 budget: WorkBudget,
442 cancellation: &CancellationToken,
443 ) -> Result<OperationReceipt<Option<LazyShadow>>, LazyWorkspaceError> {
444 measured_lazy_store_read(budget, cancellation)?;
445 let value = self.load_lazy_shadow(shadow).await.map_err(store_error)?;
446 cancellation
447 .check()
448 .map_err(|_| LazyWorkspaceError::Cancelled)?;
449 Ok(OperationReceipt {
450 value,
451 work: lazy_store_read_work(),
452 })
453 }
454
455 async fn put_lazy_shadow(
457 &self,
458 shadow: LazyShadowId,
459 value: LazyShadow,
460 ) -> Result<(), Self::Error>;
461}
462
463#[derive(Clone, Default)]
465pub struct MemoryLazyWorkspaceStore {
466 inner: Arc<Mutex<MemoryLazyWorkspaceState>>,
467}
468
469#[derive(Default)]
470struct MemoryLazyWorkspaceState {
471 workspaces: BTreeMap<WorkspaceId, LazyWorkspaceState>,
472 overlays: BTreeMap<LazyOverlayId, LazyOverlay>,
473 shadows: BTreeMap<LazyShadowId, LazyShadow>,
474}
475
476#[async_trait]
477impl LazyWorkspaceStore for MemoryLazyWorkspaceStore {
478 type Error = Infallible;
479
480 async fn load_lazy_workspace(
481 &self,
482 workspace_id: WorkspaceId,
483 ) -> Result<Option<LazyWorkspaceState>, Self::Error> {
484 let state = self
485 .inner
486 .lock()
487 .unwrap_or_else(|poisoned| poisoned.into_inner());
488 Ok(state.workspaces.get(&workspace_id).cloned())
489 }
490
491 async fn compare_and_swap_lazy_workspace(
492 &self,
493 workspace_id: WorkspaceId,
494 expected_revision: u64,
495 replacement: LazyWorkspaceState,
496 ) -> Result<bool, Self::Error> {
497 let mut state = self
498 .inner
499 .lock()
500 .unwrap_or_else(|poisoned| poisoned.into_inner());
501 let actual = state
502 .workspaces
503 .get(&workspace_id)
504 .map_or(0, |current| current.revision);
505 if actual != expected_revision {
506 return Ok(false);
507 }
508 state.workspaces.insert(workspace_id, replacement);
509 Ok(true)
510 }
511
512 async fn load_lazy_overlay(
513 &self,
514 overlay: LazyOverlayId,
515 ) -> Result<Option<LazyOverlay>, Self::Error> {
516 let state = self
517 .inner
518 .lock()
519 .unwrap_or_else(|poisoned| poisoned.into_inner());
520 Ok(state.overlays.get(&overlay).cloned())
521 }
522
523 async fn put_lazy_overlay(
524 &self,
525 overlay: LazyOverlayId,
526 value: LazyOverlay,
527 ) -> Result<(), Self::Error> {
528 let mut state = self
529 .inner
530 .lock()
531 .unwrap_or_else(|poisoned| poisoned.into_inner());
532 if let Some(existing) = state.overlays.get(&overlay) {
533 assert_eq!(existing, &value, "content-addressed overlay collision");
534 } else {
535 state.overlays.insert(overlay, value);
536 }
537 Ok(())
538 }
539
540 async fn load_lazy_shadow(
541 &self,
542 shadow: LazyShadowId,
543 ) -> Result<Option<LazyShadow>, Self::Error> {
544 let state = self
545 .inner
546 .lock()
547 .unwrap_or_else(|poisoned| poisoned.into_inner());
548 Ok(state.shadows.get(&shadow).cloned())
549 }
550
551 async fn put_lazy_shadow(
552 &self,
553 shadow: LazyShadowId,
554 value: LazyShadow,
555 ) -> Result<(), Self::Error> {
556 let mut state = self
557 .inner
558 .lock()
559 .unwrap_or_else(|poisoned| poisoned.into_inner());
560 if let Some(existing) = state.shadows.get(&shadow) {
561 assert_eq!(existing, &value, "content-addressed shadow collision");
562 } else {
563 state.shadows.insert(shadow, value);
564 }
565 Ok(())
566 }
567}
568
569#[derive(Clone)]
570struct ImmutableTreapNode<K, V, I> {
571 key: K,
572 priority: u64,
573 value: V,
574 left: I,
575 right: I,
576}
577
578#[async_trait]
579trait ImmutableTreap: Sync {
580 type Id: Copy + Send + Sync;
581 type Key: Clone + Ord + Send + Sync;
582 type Value: Clone + Send + Sync;
583
584 async fn load(
585 &self,
586 id: Self::Id,
587 ) -> Result<Option<ImmutableTreapNode<Self::Key, Self::Value, Self::Id>>, LazyWorkspaceError>;
588
589 async fn store(
590 &self,
591 node: ImmutableTreapNode<Self::Key, Self::Value, Self::Id>,
592 ) -> Result<Self::Id, LazyWorkspaceError>;
593
594 fn priority(key: &Self::Key) -> u64;
595}
596
597fn insert_immutable_treap<'a, T: ImmutableTreap>(
598 treap: &'a T,
599 root: T::Id,
600 key: T::Key,
601 value: T::Value,
602) -> LazyFuture<'a, Result<T::Id, LazyWorkspaceError>> {
603 Box::pin(async move {
604 let Some(current) = treap.load(root).await? else {
605 return treap
606 .store(ImmutableTreapNode {
607 priority: T::priority(&key),
608 key,
609 value,
610 left: root,
611 right: root,
612 })
613 .await;
614 };
615 match key.cmp(¤t.key) {
616 std::cmp::Ordering::Equal => {
617 treap
618 .store(ImmutableTreapNode {
619 key,
620 value,
621 ..current
622 })
623 .await
624 }
625 ordering => {
626 let descend_left = ordering == std::cmp::Ordering::Less;
627 let child_root = if descend_left {
628 current.left
629 } else {
630 current.right
631 };
632 let inserted = insert_immutable_treap(treap, child_root, key, value).await?;
633 let child = treap.load(inserted).await?.ok_or_else(|| {
634 LazyWorkspaceError::Store("inserted lazy index node is absent".to_owned())
635 })?;
636 if child.priority > current.priority {
637 let rotated = treap
638 .store(if descend_left {
639 ImmutableTreapNode {
640 left: child.right,
641 ..current
642 }
643 } else {
644 ImmutableTreapNode {
645 right: child.left,
646 ..current
647 }
648 })
649 .await?;
650 return treap
651 .store(if descend_left {
652 ImmutableTreapNode {
653 right: rotated,
654 ..child
655 }
656 } else {
657 ImmutableTreapNode {
658 left: rotated,
659 ..child
660 }
661 })
662 .await;
663 }
664 treap
665 .store(if descend_left {
666 ImmutableTreapNode {
667 left: inserted,
668 ..current
669 }
670 } else {
671 ImmutableTreapNode {
672 right: inserted,
673 ..current
674 }
675 })
676 .await
677 }
678 }
679 })
680}
681
682async fn immutable_treap_value<T: ImmutableTreap>(
683 treap: &T,
684 mut root: T::Id,
685 key: &T::Key,
686) -> Result<Option<T::Value>, LazyWorkspaceError> {
687 loop {
688 let Some(node) = treap.load(root).await? else {
689 return Ok(None);
690 };
691 match key.cmp(&node.key) {
692 std::cmp::Ordering::Equal => return Ok(Some(node.value)),
693 std::cmp::Ordering::Less => root = node.left,
694 std::cmp::Ordering::Greater => root = node.right,
695 }
696 }
697}
698
699struct OverlayTreap<'a, S>(&'a S);
700
701#[async_trait]
702impl<S: LazyWorkspaceStore> ImmutableTreap for OverlayTreap<'_, S> {
703 type Id = LazyOverlayId;
704 type Key = String;
705 type Value = LazyOverlayChange;
706
707 async fn load(
708 &self,
709 id: Self::Id,
710 ) -> Result<Option<ImmutableTreapNode<Self::Key, Self::Value, Self::Id>>, LazyWorkspaceError>
711 {
712 match self.0.load_lazy_overlay(id).await.map_err(store_error)? {
713 Some(LazyOverlay::Empty) => Ok(None),
714 Some(LazyOverlay::Node {
715 path,
716 priority,
717 change,
718 left,
719 right,
720 }) => Ok(Some(ImmutableTreapNode {
721 key: path,
722 priority,
723 value: change,
724 left,
725 right,
726 })),
727 None => Err(LazyWorkspaceError::Store(
728 "lazy overlay is absent".to_owned(),
729 )),
730 }
731 }
732
733 async fn store(
734 &self,
735 node: ImmutableTreapNode<Self::Key, Self::Value, Self::Id>,
736 ) -> Result<Self::Id, LazyWorkspaceError> {
737 let overlay = LazyOverlay::Node {
738 path: node.key,
739 priority: node.priority,
740 change: node.value,
741 left: node.left,
742 right: node.right,
743 };
744 let id = overlay.id()?;
745 self.0
746 .put_lazy_overlay(id, overlay)
747 .await
748 .map_err(store_error)?;
749 Ok(id)
750 }
751
752 fn priority(key: &Self::Key) -> u64 {
753 path_priority(key)
754 }
755}
756
757#[derive(Clone)]
758struct ShadowValue {
759 record: Vec<u8>,
760 metadata: Box<crate::WorkspaceMetadata>,
761}
762
763struct ShadowTreap<'a, S>(&'a S);
764
765#[async_trait]
766impl<S: LazyWorkspaceStore> ImmutableTreap for ShadowTreap<'_, S> {
767 type Id = LazyShadowId;
768 type Key = FileId;
769 type Value = ShadowValue;
770
771 async fn load(
772 &self,
773 id: Self::Id,
774 ) -> Result<Option<ImmutableTreapNode<Self::Key, Self::Value, Self::Id>>, LazyWorkspaceError>
775 {
776 match self.0.load_lazy_shadow(id).await.map_err(store_error)? {
777 Some(LazyShadow::Empty) => Ok(None),
778 Some(LazyShadow::Node {
779 file_id,
780 priority,
781 record,
782 metadata,
783 left,
784 right,
785 }) => Ok(Some(ImmutableTreapNode {
786 key: file_id,
787 priority,
788 value: ShadowValue { record, metadata },
789 left,
790 right,
791 })),
792 None => Err(LazyWorkspaceError::Store(
793 "lazy identity shadow is absent".to_owned(),
794 )),
795 }
796 }
797
798 async fn store(
799 &self,
800 node: ImmutableTreapNode<Self::Key, Self::Value, Self::Id>,
801 ) -> Result<Self::Id, LazyWorkspaceError> {
802 let shadow = LazyShadow::Node {
803 file_id: node.key,
804 priority: node.priority,
805 record: node.value.record,
806 metadata: node.value.metadata,
807 left: node.left,
808 right: node.right,
809 };
810 let id = shadow.id()?;
811 self.0
812 .put_lazy_shadow(id, shadow)
813 .await
814 .map_err(store_error)?;
815 Ok(id)
816 }
817
818 fn priority(key: &Self::Key) -> u64 {
819 shadow_priority(*key)
820 }
821}
822
823pub struct LazyWorkspace<A, O, D, S> {
825 workspace: Workspace<A, O>,
826 source: Arc<D>,
827 store: S,
828}
829
830impl<A, O, D, S> Clone for LazyWorkspace<A, O, D, S>
831where
832 S: Clone,
833{
834 fn clone(&self) -> Self {
835 Self {
836 workspace: self.workspace.clone(),
837 source: Arc::clone(&self.source),
838 store: self.store.clone(),
839 }
840 }
841}
842
843#[derive(Clone, Copy, Debug, Eq, PartialEq)]
845pub struct LazyStat {
846 pub kind: SourceNodeKind,
848 pub logical_bytes: Option<u64>,
850 pub authored: bool,
852}
853
854#[derive(Clone, Debug, Eq, PartialEq)]
856pub enum LazyLookup {
857 Authored {
859 path: String,
862 stat: WorkspaceStat,
864 },
865 Shadow {
867 record: FileRecord,
869 metadata: crate::WorkspaceMetadata,
871 source_link_count: u64,
873 },
874 Source(SourceNode),
876}
877
878struct ResolvedLazyLookup {
879 lookup: LazyLookup,
880 source: Option<SourceReference>,
881}
882
883struct PinnedSourceNode {
884 source: SourceReference,
885 node: SourceNode,
886}
887
888struct DemandBlobSource<'a, D> {
891 provider: &'a D,
892 source: SourceReference,
893 path: NamespacePath,
894 version: SourceVersion,
895 offset: u64,
896 length: u64,
897 work: Option<WorkCounters>,
898 failure: Option<DemandError>,
899}
900
901impl<D: DemandSource> crate::kernel::AsyncBlobSource for DemandBlobSource<'_, D> {
902 async fn read<'a>(
903 &'a mut self,
904 destination: &'a mut [u8],
905 cancellation: &'a CancellationToken,
906 ) -> std::io::Result<usize> {
907 let remaining = self.length.saturating_sub(self.offset);
908 if remaining == 0 {
909 self.work = Some(WorkCounters::default());
910 return Ok(0);
911 }
912 let requested = remaining.min(u64::try_from(destination.len()).unwrap_or(u64::MAX));
913 let receipt = self
914 .provider
915 .read_range(
916 self.source,
917 &self.path,
918 self.version,
919 self.offset,
920 requested,
921 cancellation,
922 )
923 .await;
924 let receipt = match receipt {
925 Ok(receipt) => receipt,
926 Err(failure) => {
927 self.work = Some(*failure.work);
928 self.failure = Some(failure.error);
929 return Err(std::io::Error::other("version-pinned demand read failed"));
930 }
931 };
932 self.work = Some(receipt.work);
933 let chunk = receipt.value;
934 if chunk.is_empty() || u64::try_from(chunk.len()).unwrap_or(u64::MAX) > requested {
935 self.failure = Some(DemandError::StaleVersion);
936 return Err(std::io::Error::other(
937 "version-pinned demand read was incomplete",
938 ));
939 }
940 self.offset = self
941 .offset
942 .checked_add(u64::try_from(chunk.len()).unwrap_or(u64::MAX))
943 .ok_or_else(|| std::io::Error::other("demand source offset overflow"))?;
944 destination
945 .get_mut(..chunk.len())
946 .ok_or_else(|| std::io::Error::other("demand source exceeded read bound"))?
947 .copy_from_slice(&chunk);
948 Ok(chunk.len())
949 }
950
951 fn take_work(&mut self) -> Option<WorkCounters> {
952 self.work.take()
953 }
954}
955
956#[derive(Clone, Copy, Debug, Eq, PartialEq)]
958pub enum LazySeekTarget {
959 Data,
961 Hole,
963}
964
965#[derive(Clone, Debug, Eq, PartialEq)]
967pub struct LazyDirectoryEntry {
968 pub name: LogicalName,
970 pub kind: SourceNodeKind,
972 pub authored: bool,
974}
975
976#[derive(Clone, Debug, Eq, PartialEq)]
978pub struct LazyDirectoryCursor {
979 source: SourceReference,
980 overlay: LazyOverlayId,
981 directory: NamespacePath,
982 phase: LazyDirectoryPhase,
983}
984
985#[derive(Clone, Debug, Eq, PartialEq)]
986enum LazyDirectoryPhase {
987 Source(Option<SourceCursor>),
988 Authored(Option<LogicalName>),
989}
990
991#[derive(Clone, Debug, Eq, PartialEq)]
993pub struct LazyDirectoryPage {
994 pub entries: Vec<LazyDirectoryEntry>,
996 pub next: Option<LazyDirectoryCursor>,
998}
999
1000#[derive(Debug, Error)]
1002pub enum LazyWorkspaceError {
1003 #[error("workspace operation failed: {0}")]
1005 Workspace(String),
1006 #[error("source demand failed: {0}")]
1008 Demand(#[from] DemandError),
1009 #[error("lazy state failed: {0}")]
1011 Store(String),
1012 #[error("lazy state changed concurrently")]
1014 Concurrent,
1015 #[error("lazy source binding is stale")]
1017 StaleSource,
1018 #[error("path is absent")]
1020 NotFound,
1021 #[error("path is not a directory")]
1023 NotDirectory,
1024 #[error("path is not a regular file")]
1026 NotRegularFile,
1027 #[error("file exceeds the requested read bound")]
1029 TooLarge,
1030 #[error("directory page bound must be positive")]
1032 InvalidPageBound,
1033 #[error("lazy directory cursor is stale")]
1035 StaleCursor,
1036 #[error("lazy filesystem object identity is stale")]
1038 StaleIdentity,
1039 #[error("source node cannot be represented by the authored workspace")]
1041 UnsupportedNode,
1042 #[error("source hard-link aliases must be promoted before removing the final authored binding")]
1045 UnresolvedHardLinks,
1046 #[error("lazy exact scan was cancelled")]
1048 Cancelled,
1049 #[error("lazy exact scan exceeded its work budget: {0}")]
1051 Work(String),
1052}
1053
1054#[derive(Clone, Copy)]
1055struct ExactificationSource(SourceReference);
1056
1057impl LazySnapshotRef {
1058 fn exactification_source(
1059 self,
1060 live_source: SourceReference,
1061 ) -> Result<ExactificationSource, LazyWorkspaceError> {
1062 if self.source.identity != live_source.identity {
1063 return Err(LazyWorkspaceError::StaleSource);
1064 }
1065 Ok(ExactificationSource(live_source))
1066 }
1067
1068 fn exactification_state(
1069 self,
1070 workspace_id: WorkspaceId,
1071 parent_workspace_id: WorkspaceId,
1072 source: ExactificationSource,
1073 ) -> LazyWorkspaceState {
1074 LazyWorkspaceState {
1075 schema_version: LAZY_STATE_SCHEMA,
1076 revision: 1,
1077 workspace_id,
1078 parent_workspace_id: Some(parent_workspace_id),
1079 source: source.0,
1082 overlay: self.overlay,
1083 shadows: self.shadows,
1084 pending_remove: None,
1085 }
1086 }
1087}
1088
1089impl<A, O, D, S> LazyWorkspace<A, O, D, S>
1090where
1091 A: AsyncAuthorityStore,
1092 O: AsyncObjectStore,
1093 D: DemandSource + 'static,
1094 S: LazyWorkspaceStore,
1095{
1096 pub async fn attach(
1098 fs: &Fs<A, O>,
1099 name: impl AsRef<str>,
1100 source: Arc<D>,
1101 store: S,
1102 ) -> Result<Self, LazyWorkspaceError> {
1103 let workspace = fs.create_workspace(name).await.map_err(workspace_error)?;
1104 Self::attach_workspace(workspace, source, store).await
1105 }
1106
1107 pub async fn attach_with_config(
1110 fs: &Fs<A, O>,
1111 name: impl AsRef<str>,
1112 source: Arc<D>,
1113 store: S,
1114 config: VolumeConfig,
1115 ) -> Result<Self, LazyWorkspaceError> {
1116 let workspace = fs
1117 .create_workspace_with_config(name, config)
1118 .await
1119 .map_err(workspace_error)?;
1120 Self::attach_workspace(workspace, source, store).await
1121 }
1122
1123 async fn attach_workspace(
1124 workspace: Workspace<A, O>,
1125 source: Arc<D>,
1126 store: S,
1127 ) -> Result<Self, LazyWorkspaceError> {
1128 let overlay = LazyOverlay::default();
1129 let overlay_id = overlay.id()?;
1130 store
1131 .put_lazy_overlay(overlay_id, overlay)
1132 .await
1133 .map_err(store_error)?;
1134 let shadows = LazyShadow::default();
1135 let shadow_id = shadows.id()?;
1136 store
1137 .put_lazy_shadow(shadow_id, shadows)
1138 .await
1139 .map_err(store_error)?;
1140 let state = LazyWorkspaceState {
1141 schema_version: LAZY_STATE_SCHEMA,
1142 revision: 1,
1143 workspace_id: workspace.id(),
1144 parent_workspace_id: None,
1145 source: source.reference(),
1146 overlay: overlay_id,
1147 shadows: shadow_id,
1148 pending_remove: None,
1149 };
1150 let existing = store
1151 .load_lazy_workspace(workspace.id())
1152 .await
1153 .map_err(store_error)?;
1154 match existing {
1155 Some(existing)
1156 if existing.source == state.source
1157 && existing.schema_version == LAZY_STATE_SCHEMA => {}
1158 Some(_) => return Err(LazyWorkspaceError::StaleSource),
1159 None => {
1160 if !store
1161 .compare_and_swap_lazy_workspace(workspace.id(), 0, state)
1162 .await
1163 .map_err(store_error)?
1164 {
1165 return Err(LazyWorkspaceError::Concurrent);
1166 }
1167 }
1168 }
1169 let lazy = Self {
1170 workspace,
1171 source,
1172 store,
1173 };
1174 lazy.recover_pending_remove().await?;
1175 Ok(lazy)
1176 }
1177
1178 pub async fn open(
1180 workspace: Workspace<A, O>,
1181 source: Arc<D>,
1182 store: S,
1183 ) -> Result<Self, LazyWorkspaceError> {
1184 let state = store
1185 .load_lazy_workspace(workspace.id())
1186 .await
1187 .map_err(store_error)?
1188 .ok_or_else(|| LazyWorkspaceError::Store("lazy binding is absent".to_owned()))?;
1189 if state.schema_version != LAZY_STATE_SCHEMA
1190 || state.workspace_id != workspace.id()
1191 || state.source.identity != source.reference().identity
1192 {
1193 return Err(LazyWorkspaceError::StaleSource);
1194 }
1195 if store
1196 .load_lazy_overlay(state.overlay)
1197 .await
1198 .map_err(store_error)?
1199 .is_none()
1200 {
1201 return Err(LazyWorkspaceError::Store(
1202 "lazy overlay is absent".to_owned(),
1203 ));
1204 }
1205 if store
1206 .load_lazy_shadow(state.shadows)
1207 .await
1208 .map_err(store_error)?
1209 .is_none()
1210 {
1211 return Err(LazyWorkspaceError::Store(
1212 "lazy identity shadow is absent".to_owned(),
1213 ));
1214 }
1215 let lazy = Self {
1216 workspace,
1217 source,
1218 store,
1219 };
1220 lazy.recover_pending_remove().await?;
1221 lazy.rebind_source().await?;
1222 Ok(lazy)
1223 }
1224
1225 #[must_use]
1227 pub const fn workspace(&self) -> &Workspace<A, O> {
1228 &self.workspace
1229 }
1230
1231 pub async fn open_related(&self, workspace: Workspace<A, O>) -> Result<Self, LazyWorkspaceError>
1236 where
1237 S: Clone,
1238 {
1239 Self::open(workspace, Arc::clone(&self.source), self.store.clone()).await
1240 }
1241
1242 pub async fn snapshot(&self) -> Result<LazySnapshotRef, LazyWorkspaceError> {
1244 let state = self.state().await?;
1245 let authored_generation = self.workspace.head().await.map_err(workspace_error)?.id();
1246 Ok(LazySnapshotRef::new(
1247 self.workspace.id(),
1248 authored_generation,
1249 state.source,
1250 state.overlay,
1251 state.shadows,
1252 ))
1253 }
1254
1255 pub async fn binding(&self) -> Result<LazyWorkspaceState, LazyWorkspaceError> {
1257 self.state().await
1258 }
1259
1260 pub async fn fork(
1262 &self,
1263 destination: impl AsRef<str>,
1264 idempotency_key: IdempotencyKey,
1265 ) -> Result<Self, LazyWorkspaceError> {
1266 let parent = self.state().await?;
1267 let generation = self.workspace.head().await.map_err(workspace_error)?;
1268 let workspace = self
1269 .workspace
1270 .fork(
1271 destination,
1272 ForkOptions {
1273 generation,
1274 idempotency_key,
1275 },
1276 )
1277 .await
1278 .map_err(workspace_error)?;
1279 let state = LazyWorkspaceState {
1280 schema_version: LAZY_STATE_SCHEMA,
1281 revision: 1,
1282 workspace_id: workspace.id(),
1283 parent_workspace_id: Some(self.workspace.id()),
1284 source: parent.source,
1285 overlay: parent.overlay,
1286 shadows: parent.shadows,
1287 pending_remove: None,
1288 };
1289 if !self
1290 .store
1291 .compare_and_swap_lazy_workspace(workspace.id(), 0, state)
1292 .await
1293 .map_err(store_error)?
1294 {
1295 let existing = self
1296 .store
1297 .load_lazy_workspace(workspace.id())
1298 .await
1299 .map_err(store_error)?
1300 .ok_or(LazyWorkspaceError::Concurrent)?;
1301 if existing.parent_workspace_id != Some(self.workspace.id())
1302 || existing.source != parent.source
1303 || existing.overlay != parent.overlay
1304 || existing.shadows != parent.shadows
1305 {
1306 return Err(LazyWorkspaceError::Concurrent);
1307 }
1308 }
1309 Ok(Self {
1310 workspace,
1311 source: Arc::clone(&self.source),
1312 store: self.store.clone(),
1313 })
1314 }
1315
1316 #[allow(clippy::too_many_lines)]
1322 pub async fn exactify_snapshot(
1323 &self,
1324 snapshot: LazySnapshotRef,
1325 destination: impl AsRef<str>,
1326 idempotency_key: IdempotencyKey,
1327 budget: WorkBudget,
1328 cancellation: &CancellationToken,
1329 ) -> Result<OperationReceipt<crate::Generation<A, O>>, LazyWorkspaceError> {
1330 cancellation
1331 .check()
1332 .map_err(|_| LazyWorkspaceError::Cancelled)?;
1333 if snapshot.workspace_id != self.workspace.id() {
1334 return Err(LazyWorkspaceError::StaleSource);
1335 }
1336 let exactification_source = snapshot.exactification_source(self.source.reference())?;
1337 let overlay = self
1338 .store
1339 .load_lazy_overlay_measured(snapshot.overlay, budget, cancellation)
1340 .await?;
1341 let mut work = overlay.work;
1342 let shadow = self
1343 .store
1344 .load_lazy_shadow_measured(
1345 snapshot.shadows,
1346 remaining_work(work, budget)?,
1347 cancellation,
1348 )
1349 .await?;
1350 work = account_work(work, shadow.work, budget)?;
1351 if overlay.value.is_none() || shadow.value.is_none() {
1352 return Err(LazyWorkspaceError::Store(
1353 "lazy snapshot index is absent".to_owned(),
1354 ));
1355 }
1356 let generation = self
1357 .workspace
1358 .generation_measured(
1359 snapshot.authored_generation,
1360 remaining_work(work, budget)?,
1361 cancellation,
1362 )
1363 .await
1364 .map_err(workspace_error)?;
1365 work = account_work(work, generation.work, budget)?;
1366 let workspace = self
1367 .workspace
1368 .fork_measured(
1369 destination,
1370 ForkOptions {
1371 generation: generation.value,
1372 idempotency_key,
1373 },
1374 remaining_work(work, budget)?,
1375 cancellation,
1376 )
1377 .await
1378 .map_err(workspace_error)?;
1379 work = account_work(work, workspace.work, budget)?;
1380 let workspace = workspace.value;
1381 let state = snapshot.exactification_state(
1382 workspace.id(),
1383 self.workspace.id(),
1384 exactification_source,
1385 );
1386 let inserted = self
1387 .store
1388 .compare_and_swap_lazy_workspace_measured(
1389 workspace.id(),
1390 0,
1391 state.clone(),
1392 remaining_work(work, budget)?,
1393 cancellation,
1394 )
1395 .await?;
1396 work = account_work(work, inserted.work, budget)?;
1397 if !inserted.value {
1398 let existing = self
1399 .store
1400 .load_lazy_workspace_measured(
1401 workspace.id(),
1402 remaining_work(work, budget)?,
1403 cancellation,
1404 )
1405 .await?;
1406 work = account_work(work, existing.work, budget)?;
1407 let existing = existing.value.ok_or(LazyWorkspaceError::Concurrent)?;
1408 if existing.parent_workspace_id != state.parent_workspace_id
1409 || existing.source != state.source
1410 || existing.overlay != state.overlay
1411 || existing.shadows != state.shadows
1412 {
1413 return Err(LazyWorkspaceError::Concurrent);
1414 }
1415 }
1416 let exactified = Self {
1417 workspace,
1418 source: Arc::clone(&self.source),
1419 store: self.store.clone(),
1420 }
1421 .exactify(remaining_work(work, budget)?, cancellation)
1422 .await?;
1423 work = account_work(work, exactified.work, budget)?;
1424 Ok(OperationReceipt {
1425 value: exactified.value,
1426 work,
1427 })
1428 }
1429
1430 pub async fn rebind_source(&self) -> Result<LazyWorkspaceState, LazyWorkspaceError> {
1435 let source = self.source.reference();
1436 for _ in 0..MAXIMUM_STATE_RETRIES {
1437 let state = self
1438 .store
1439 .load_lazy_workspace(self.workspace.id())
1440 .await
1441 .map_err(store_error)?
1442 .ok_or_else(|| LazyWorkspaceError::Store("lazy binding is absent".to_owned()))?;
1443 if state.schema_version != LAZY_STATE_SCHEMA
1444 || state.workspace_id != self.workspace.id()
1445 || state.source.identity != source.identity
1446 {
1447 return Err(LazyWorkspaceError::StaleSource);
1448 }
1449 if state.pending_remove.is_some() {
1450 return Err(LazyWorkspaceError::Concurrent);
1451 }
1452 if state.source == source {
1453 return Ok(state);
1454 }
1455 let replacement = LazyWorkspaceState {
1456 revision: state.revision.saturating_add(1),
1457 source,
1458 ..state.clone()
1459 };
1460 if self
1461 .store
1462 .compare_and_swap_lazy_workspace(
1463 self.workspace.id(),
1464 state.revision,
1465 replacement.clone(),
1466 )
1467 .await
1468 .map_err(store_error)?
1469 {
1470 return Ok(replacement);
1471 }
1472 }
1473 Err(LazyWorkspaceError::Concurrent)
1474 }
1475
1476 pub async fn stat(&self, path: &str) -> Result<LazyStat, LazyWorkspaceError> {
1478 let cancellation = CancellationToken::new();
1479 self.stat_measured(path, WorkBudget::UNBOUNDED, &cancellation)
1480 .await
1481 .map(|receipt| receipt.value)
1482 }
1483
1484 async fn stat_measured(
1485 &self,
1486 path: &str,
1487 budget: WorkBudget,
1488 cancellation: &CancellationToken,
1489 ) -> Result<OperationReceipt<LazyStat>, LazyWorkspaceError> {
1490 let receipt = self.lookup_measured(path, budget, cancellation).await?;
1491 let value = match receipt.value.lookup {
1492 LazyLookup::Authored { stat, .. } => LazyStat {
1493 kind: source_kind(stat.kind),
1494 logical_bytes: stat.logical_bytes,
1495 authored: true,
1496 },
1497 LazyLookup::Shadow { record, .. } => LazyStat {
1498 kind: source_kind(record.kind),
1499 logical_bytes: record_logical_bytes(record),
1500 authored: true,
1501 },
1502 LazyLookup::Source(node) => LazyStat {
1503 kind: node.kind,
1504 logical_bytes: node.logical_bytes,
1505 authored: false,
1506 },
1507 };
1508 Ok(OperationReceipt {
1509 value,
1510 work: receipt.work,
1511 })
1512 }
1513
1514 pub async fn lookup(&self, path: &str) -> Result<LazyLookup, LazyWorkspaceError> {
1516 let cancellation = CancellationToken::new();
1517 self.lookup_measured(path, WorkBudget::UNBOUNDED, &cancellation)
1518 .await
1519 .map(|receipt| receipt.value.lookup)
1520 }
1521
1522 #[cfg(all(feature = "native-mount", not(unix)))]
1523 pub(crate) async fn lookup_resolved(
1524 &self,
1525 path: &str,
1526 ) -> Result<(LazyLookup, Option<SourceReference>), LazyWorkspaceError> {
1527 let cancellation = CancellationToken::new();
1528 self.lookup_measured(path, WorkBudget::UNBOUNDED, &cancellation)
1529 .await
1530 .map(|receipt| (receipt.value.lookup, receipt.value.source))
1531 }
1532
1533 async fn lookup_measured(
1534 &self,
1535 path: &str,
1536 budget: WorkBudget,
1537 cancellation: &CancellationToken,
1538 ) -> Result<OperationReceipt<ResolvedLazyLookup>, LazyWorkspaceError> {
1539 let authored = self
1540 .workspace
1541 .stat_optional_measured(path, budget, cancellation)
1542 .await
1543 .map_err(workspace_error)?;
1544 let mut work = authored.work;
1545 if let Some(stat) = authored.value {
1546 return Ok(OperationReceipt {
1547 value: ResolvedLazyLookup {
1548 lookup: LazyLookup::Authored {
1549 path: self.canonical_path(path)?,
1550 stat,
1551 },
1552 source: None,
1553 },
1554 work,
1555 });
1556 }
1557 let state_receipt = self
1558 .state_measured(remaining_work(work, budget)?, cancellation)
1559 .await?;
1560 work = account_work(work, state_receipt.work, budget)?;
1561 let state = state_receipt.value;
1562 let tombstoned = self
1563 .tombstoned_measured(
1564 state.overlay,
1565 path,
1566 remaining_work(work, budget)?,
1567 cancellation,
1568 )
1569 .await?;
1570 work = account_work(work, tombstoned.work, budget)?;
1571 if tombstoned.value {
1572 return Err(LazyWorkspaceError::NotFound);
1573 }
1574 let observed = self.observe_measured(path, state, cancellation).await?;
1575 work = account_work(work, observed.work, budget)?;
1576 let (source, node) = observed.value;
1577 let authored = self
1578 .authored_alias_measured(&node, remaining_work(work, budget)?, cancellation)
1579 .await?;
1580 work = account_work(work, authored.work, budget)?;
1581 if let Some(authored) = authored.value {
1582 return Ok(OperationReceipt {
1583 value: ResolvedLazyLookup {
1584 lookup: authored,
1585 source: None,
1586 },
1587 work,
1588 });
1589 }
1590 Ok(OperationReceipt {
1591 value: ResolvedLazyLookup {
1592 lookup: LazyLookup::Source(node),
1593 source: Some(source),
1594 },
1595 work,
1596 })
1597 }
1598
1599 pub async fn inspect(&self, path: &str) -> Result<LazyLookup, LazyWorkspaceError> {
1604 let cancellation = CancellationToken::new();
1605 self.inspect_measured(path, WorkBudget::UNBOUNDED, &cancellation)
1606 .await
1607 .map(|receipt| receipt.value.lookup)
1608 }
1609
1610 #[cfg(any(unix, test))]
1611 pub(crate) async fn inspect_resolved(
1612 &self,
1613 path: &str,
1614 ) -> Result<(LazyLookup, Option<SourceReference>), LazyWorkspaceError> {
1615 let cancellation = CancellationToken::new();
1616 self.inspect_measured(path, WorkBudget::UNBOUNDED, &cancellation)
1617 .await
1618 .map(|receipt| (receipt.value.lookup, receipt.value.source))
1619 }
1620
1621 async fn inspect_measured(
1622 &self,
1623 path: &str,
1624 budget: WorkBudget,
1625 cancellation: &CancellationToken,
1626 ) -> Result<OperationReceipt<ResolvedLazyLookup>, LazyWorkspaceError> {
1627 self.inspect_with_observation_policy(path, budget, cancellation, false, false)
1628 .await
1629 }
1630
1631 async fn inspect_snapshot_measured(
1632 &self,
1633 path: &str,
1634 budget: WorkBudget,
1635 cancellation: &CancellationToken,
1636 ) -> Result<OperationReceipt<ResolvedLazyLookup>, LazyWorkspaceError> {
1637 self.inspect_with_observation_policy(path, budget, cancellation, true, false)
1638 .await
1639 }
1640
1641 async fn inspect_snapshot_after_authored_absence(
1642 &self,
1643 path: &str,
1644 budget: WorkBudget,
1645 cancellation: &CancellationToken,
1646 ) -> Result<OperationReceipt<ResolvedLazyLookup>, LazyWorkspaceError> {
1647 self.inspect_with_observation_policy(path, budget, cancellation, true, true)
1648 .await
1649 }
1650
1651 async fn inspect_with_observation_policy(
1652 &self,
1653 path: &str,
1654 budget: WorkBudget,
1655 cancellation: &CancellationToken,
1656 retain_pinned_observation: bool,
1657 authored_absent: bool,
1658 ) -> Result<OperationReceipt<ResolvedLazyLookup>, LazyWorkspaceError> {
1659 let authored = if authored_absent {
1660 OperationReceipt {
1661 value: None,
1662 work: WorkCounters::default(),
1663 }
1664 } else {
1665 self.workspace
1666 .stat_optional_measured(path, budget, cancellation)
1667 .await
1668 .map_err(workspace_error)?
1669 };
1670 let mut work = authored.work;
1671 if let Some(stat) = authored.value {
1672 return Ok(OperationReceipt {
1673 value: ResolvedLazyLookup {
1674 lookup: LazyLookup::Authored {
1675 path: self.canonical_path(path)?,
1676 stat,
1677 },
1678 source: None,
1679 },
1680 work,
1681 });
1682 }
1683 let state_receipt = self
1684 .state_measured(remaining_work(work, budget)?, cancellation)
1685 .await?;
1686 work = account_work(work, state_receipt.work, budget)?;
1687 let state = state_receipt.value;
1688 let fact = self
1689 .overlay_fact_measured(
1690 state.overlay,
1691 path,
1692 remaining_work(work, budget)?,
1693 cancellation,
1694 )
1695 .await?;
1696 work = account_work(work, fact.work, budget)?;
1697 let observed = match fact.value {
1698 Some(LazyOverlayChange::Tombstone) => return Err(LazyWorkspaceError::NotFound),
1699 Some(LazyOverlayChange::Observe { source, node })
1700 if retain_pinned_observation || source == state.source =>
1701 {
1702 Some((source, node))
1703 }
1704 Some(LazyOverlayChange::Observe { .. }) | None => None,
1705 };
1706 let (source, node) = if let Some(observed) = observed {
1707 observed
1708 } else {
1709 let receipt = self
1710 .source
1711 .lookup(state.source, &self.namespace_path(path)?, cancellation)
1712 .await
1713 .map_err(|failure| LazyWorkspaceError::from(failure.error))?;
1714 work = account_work(work, receipt.work, budget)?;
1715 (
1716 state.source,
1717 receipt.value.ok_or(LazyWorkspaceError::NotFound)?,
1718 )
1719 };
1720 let tombstoned = self
1721 .tombstoned_measured(
1722 state.overlay,
1723 path,
1724 remaining_work(work, budget)?,
1725 cancellation,
1726 )
1727 .await?;
1728 work = account_work(work, tombstoned.work, budget)?;
1729 if tombstoned.value {
1730 return Err(LazyWorkspaceError::NotFound);
1731 }
1732 let authored = self
1733 .authored_alias_measured(&node, remaining_work(work, budget)?, cancellation)
1734 .await?;
1735 work = account_work(work, authored.work, budget)?;
1736 if let Some(authored) = authored.value {
1737 return Ok(OperationReceipt {
1738 value: ResolvedLazyLookup {
1739 lookup: authored,
1740 source: None,
1741 },
1742 work,
1743 });
1744 }
1745 Ok(OperationReceipt {
1746 value: ResolvedLazyLookup {
1747 lookup: LazyLookup::Source(node),
1748 source: Some(source),
1749 },
1750 work,
1751 })
1752 }
1753
1754 pub fn source_file_id(&self, node: &SourceNode) -> FileId {
1759 let mut hasher = blake3::Hasher::new();
1760 hasher.update(b"acyclic-fs-lazy-source-file-id-v1\0");
1761 hasher.update(&self.source.reference().identity);
1762 hasher.update(&node.file_identity);
1763 let digest = hasher.finalize();
1764 let mut bytes = [0_u8; 16];
1765 bytes.copy_from_slice(&digest.as_bytes()[..16]);
1766 FileId::from_bytes(bytes)
1767 }
1768
1769 fn direct_file_id_for_lookup(&self, lookup: &LazyLookup) -> FileId {
1772 match lookup {
1773 LazyLookup::Source(node) => self.source_file_id(node),
1774 LazyLookup::Authored { stat, .. } => stat.file_id,
1775 LazyLookup::Shadow { record, .. } => record.file_id,
1776 }
1777 }
1778
1779 #[cfg(any(feature = "native-mount", test))]
1780 pub(crate) async fn stable_file_id_for_lookup(
1781 &self,
1782 path: &str,
1783 lookup: &LazyLookup,
1784 ) -> Result<FileId, LazyWorkspaceError> {
1785 match lookup {
1786 LazyLookup::Source(node) => Ok(self.source_file_id(node)),
1787 LazyLookup::Shadow { record, .. } => Ok(record.file_id),
1788 LazyLookup::Authored { stat, .. } if stat.kind == FileKind::Directory => {
1789 let state = self.state().await?;
1790 match self.overlay_fact(state.overlay, path).await? {
1791 Some(LazyOverlayChange::Observe { node, .. })
1792 if node.kind == SourceNodeKind::Directory =>
1793 {
1794 Ok(self.source_file_id(&node))
1795 }
1796 Some(LazyOverlayChange::Observe { .. })
1797 | Some(LazyOverlayChange::Tombstone)
1798 | None => Ok(stat.file_id),
1799 }
1800 }
1801 LazyLookup::Authored { stat, .. } => Ok(stat.file_id),
1802 }
1803 }
1804
1805 async fn authored_alias_measured(
1806 &self,
1807 node: &SourceNode,
1808 budget: WorkBudget,
1809 cancellation: &CancellationToken,
1810 ) -> Result<OperationReceipt<Option<LazyLookup>>, LazyWorkspaceError> {
1811 if node.kind == SourceNodeKind::Directory || node.link_count == Some(1) {
1816 return Ok(OperationReceipt {
1817 value: None,
1818 work: WorkCounters::default(),
1819 });
1820 }
1821 let file_id = self.source_file_id(node);
1822 let state = self.state_measured(budget, cancellation).await?;
1823 let generation = self
1824 .workspace
1825 .head_measured(remaining_work(state.work, budget)?, cancellation)
1826 .await
1827 .map_err(workspace_error)?;
1828 let mut work = account_work(state.work, generation.work, budget)?;
1829 let state = state.value;
1830 let paths = generation
1831 .value
1832 .paths_for_file_id_measured(
1833 file_id,
1834 u32::MAX,
1835 remaining_work(work, budget)?,
1836 cancellation,
1837 )
1838 .await
1839 .map_err(workspace_error)?;
1840 work = account_work(work, paths.work, budget)?;
1841 if let Some(path) = paths.value.into_iter().next() {
1842 let stat = self
1843 .workspace
1844 .stat_optional_measured(&path, remaining_work(work, budget)?, cancellation)
1845 .await
1846 .map_err(workspace_error)?;
1847 work = account_work(work, stat.work, budget)?;
1848 let mut stat = stat.value.ok_or(LazyWorkspaceError::Concurrent)?;
1849 if let Some(source_links) = node.link_count {
1850 stat.link_count = stat.link_count.max(source_links);
1851 }
1852 return Ok(OperationReceipt {
1853 value: Some(LazyLookup::Authored { path, stat }),
1854 work,
1855 });
1856 }
1857 let shadow = self
1858 .shadow_record_measured(
1859 state.shadows,
1860 file_id,
1861 remaining_work(work, budget)?,
1862 cancellation,
1863 )
1864 .await?;
1865 work = account_work(work, shadow.work, budget)?;
1866 let Some((record, metadata)) = shadow.value else {
1867 return Ok(OperationReceipt { value: None, work });
1868 };
1869 Ok(OperationReceipt {
1870 value: Some(LazyLookup::Shadow {
1871 record,
1872 metadata,
1873 source_link_count: node.link_count.unwrap_or(1),
1874 }),
1875 work,
1876 })
1877 }
1878
1879 pub async fn read_range(
1881 &self,
1882 path: &str,
1883 offset: u64,
1884 length: u64,
1885 ) -> Result<Bytes, LazyWorkspaceError> {
1886 let cancellation = CancellationToken::new();
1887 self.read_range_measured(path, offset, length, WorkBudget::UNBOUNDED, &cancellation)
1888 .await
1889 .map(|receipt| receipt.value)
1890 }
1891
1892 #[cfg(any(unix, test))]
1893 pub(crate) async fn read_source_range(
1894 &self,
1895 path: &str,
1896 source: SourceReference,
1897 node: SourceNode,
1898 offset: u64,
1899 length: u64,
1900 ) -> Result<Bytes, LazyWorkspaceError> {
1901 if node.kind != SourceNodeKind::RegularFile {
1902 return Err(LazyWorkspaceError::NotRegularFile);
1903 }
1904 self.source
1905 .read_range(
1906 source,
1907 &self.namespace_path(path)?,
1908 node.version,
1909 offset,
1910 length,
1911 &CancellationToken::new(),
1912 )
1913 .await
1914 .map(|receipt| receipt.value)
1915 .map_err(|failure| LazyWorkspaceError::from(failure.error))
1916 }
1917
1918 async fn read_range_measured(
1919 &self,
1920 path: &str,
1921 offset: u64,
1922 length: u64,
1923 budget: WorkBudget,
1924 cancellation: &CancellationToken,
1925 ) -> Result<OperationReceipt<Bytes>, LazyWorkspaceError> {
1926 let lookup = self.lookup_measured(path, budget, cancellation).await?;
1927 let prior = lookup.work;
1928 let ResolvedLazyLookup { lookup, source } = lookup.value;
1929 match lookup {
1930 LazyLookup::Authored {
1931 path: authored_path,
1932 ..
1933 } => {
1934 let value = self
1935 .workspace
1936 .read_range(&authored_path, offset, length)
1937 .await
1938 .map_err(workspace_error)?;
1939 Ok(OperationReceipt { value, work: prior })
1940 }
1941 LazyLookup::Shadow { record, .. } => {
1942 if record.kind != FileKind::Regular {
1943 return Err(LazyWorkspaceError::NotRegularFile);
1944 }
1945 let receipt = self
1946 .workspace
1947 .detached_record(record)
1948 .read_range(
1949 crate::ByteRange { offset, length },
1950 remaining_work(prior, budget)?,
1951 cancellation,
1952 )
1953 .await
1954 .map_err(|failure| LazyWorkspaceError::Workspace(failure.error.to_string()));
1955 let receipt = receipt?;
1956 Ok(OperationReceipt {
1957 value: receipt.value.bytes,
1958 work: account_work(prior, receipt.work, budget)?,
1959 })
1960 }
1961 LazyLookup::Source(node) => {
1962 if node.kind != SourceNodeKind::RegularFile {
1963 return Err(LazyWorkspaceError::NotRegularFile);
1964 }
1965 let source = source.ok_or(LazyWorkspaceError::Concurrent)?;
1966 let receipt = self
1967 .source
1968 .read_range(
1969 source,
1970 &self.namespace_path(path)?,
1971 node.version,
1972 offset,
1973 length,
1974 cancellation,
1975 )
1976 .await
1977 .map_err(|failure| LazyWorkspaceError::from(failure.error))?;
1978 Ok(OperationReceipt {
1979 value: receipt.value,
1980 work: account_work(prior, receipt.work, budget)?,
1981 })
1982 }
1983 }
1984 }
1985
1986 pub async fn read(&self, path: &str, maximum_bytes: u64) -> Result<Bytes, LazyWorkspaceError> {
1988 let stat = self.stat(path).await?;
1989 let length = stat
1990 .logical_bytes
1991 .ok_or(LazyWorkspaceError::NotRegularFile)?;
1992 if length > maximum_bytes {
1993 return Err(LazyWorkspaceError::TooLarge);
1994 }
1995 self.read_range(path, 0, length).await
1996 }
1997
1998 pub async fn read_link(&self, path: &str) -> Result<Bytes, LazyWorkspaceError> {
2000 let cancellation = CancellationToken::new();
2001 self.read_link_measured(path, WorkBudget::UNBOUNDED, &cancellation)
2002 .await
2003 .map(|receipt| receipt.value)
2004 }
2005
2006 async fn read_link_measured(
2007 &self,
2008 path: &str,
2009 budget: WorkBudget,
2010 cancellation: &CancellationToken,
2011 ) -> Result<OperationReceipt<Bytes>, LazyWorkspaceError> {
2012 let lookup = self.lookup_measured(path, budget, cancellation).await?;
2013 let prior = lookup.work;
2014 let ResolvedLazyLookup { lookup, source } = lookup.value;
2015 match lookup {
2016 LazyLookup::Authored {
2017 path: authored_path,
2018 stat,
2019 } => {
2020 if stat.kind != FileKind::SymbolicLink {
2021 return Err(LazyWorkspaceError::NotRegularFile);
2022 }
2023 let value = self
2024 .workspace
2025 .read_symbolic_link(&authored_path)
2026 .await
2027 .map_err(workspace_error)?;
2028 Ok(OperationReceipt { value, work: prior })
2029 }
2030 LazyLookup::Shadow { record, .. } => {
2031 let receipt = self
2032 .workspace
2033 .detached_record(record)
2034 .read_symbolic_link(remaining_work(prior, budget)?, cancellation)
2035 .await
2036 .map_err(|failure| LazyWorkspaceError::Workspace(failure.error.to_string()))?;
2037 Ok(OperationReceipt {
2038 value: receipt.value,
2039 work: account_work(prior, receipt.work, budget)?,
2040 })
2041 }
2042 LazyLookup::Source(node) => {
2043 if node.kind != SourceNodeKind::SymbolicLink {
2044 return Err(LazyWorkspaceError::NotRegularFile);
2045 }
2046 let source = source.ok_or(LazyWorkspaceError::Concurrent)?;
2047 let receipt = self
2048 .source
2049 .read_link(
2050 source,
2051 &self.namespace_path(path)?,
2052 node.version,
2053 cancellation,
2054 )
2055 .await
2056 .map_err(|failure| LazyWorkspaceError::from(failure.error))?;
2057 Ok(OperationReceipt {
2058 value: receipt.value,
2059 work: account_work(prior, receipt.work, budget)?,
2060 })
2061 }
2062 }
2063 }
2064
2065 pub async fn seek(
2071 &self,
2072 path: &str,
2073 offset: u64,
2074 target: LazySeekTarget,
2075 ) -> Result<Option<u64>, LazyWorkspaceError> {
2076 match self.lookup(path).await? {
2077 LazyLookup::Authored {
2078 path: authored_path,
2079 stat,
2080 } => {
2081 if stat.kind != FileKind::Regular {
2082 return Err(LazyWorkspaceError::NotRegularFile);
2083 }
2084 let length = stat
2085 .logical_bytes
2086 .ok_or(LazyWorkspaceError::NotRegularFile)?;
2087 if offset >= length {
2088 return Ok(None);
2089 }
2090 let plan = self
2091 .workspace
2092 .plan_extents(&authored_path, offset, length - offset, 65_536)
2093 .await
2094 .map_err(workspace_error)?;
2095 let found = plan.spans.into_iter().find(|span| match target {
2096 LazySeekTarget::Data => span.kind != WorkspaceExtentKind::Hole,
2097 LazySeekTarget::Hole => span.kind == WorkspaceExtentKind::Hole,
2098 });
2099 Ok(found
2100 .map(|span| span.offset)
2101 .or_else(|| (target == LazySeekTarget::Hole).then_some(length)))
2102 }
2103 LazyLookup::Shadow { record, .. } => {
2104 if record.kind != FileKind::Regular {
2105 return Err(LazyWorkspaceError::NotRegularFile);
2106 }
2107 self.workspace
2108 .detached_record(record)
2109 .seek(
2110 offset,
2111 match target {
2112 LazySeekTarget::Data => crate::kernel::ExtentSeekTarget::Data,
2113 LazySeekTarget::Hole => crate::kernel::ExtentSeekTarget::Hole,
2114 },
2115 crate::WorkBudget::UNBOUNDED,
2116 &crate::CancellationToken::new(),
2117 )
2118 .await
2119 .map(|receipt| receipt.value)
2120 .map_err(|failure| LazyWorkspaceError::Workspace(failure.error.to_string()))
2121 }
2122 LazyLookup::Source(node) => {
2123 if node.kind != SourceNodeKind::RegularFile {
2124 return Err(LazyWorkspaceError::NotRegularFile);
2125 }
2126 let length = node
2127 .logical_bytes
2128 .ok_or(LazyWorkspaceError::NotRegularFile)?;
2129 if offset >= length {
2130 return Ok(None);
2131 }
2132 Ok(Some(match target {
2133 LazySeekTarget::Data => offset,
2134 LazySeekTarget::Hole => length,
2135 }))
2136 }
2137 }
2138 }
2139
2140 #[allow(clippy::too_many_lines)]
2143 pub async fn promote(
2144 &self,
2145 path: &str,
2146 maximum_bytes: u64,
2147 idempotency_key: IdempotencyKey,
2148 ) -> Result<TransactionCommit<A, O>, LazyWorkspaceError> {
2149 self.promote_with_permit(
2150 path,
2151 maximum_bytes,
2152 idempotency_key,
2153 crate::PublicationPermit::Unrestricted,
2154 )
2155 .await
2156 }
2157
2158 #[allow(clippy::too_many_lines)]
2159 async fn populate_source_node_measured(
2160 &self,
2161 transaction: &mut crate::Transaction<A, O>,
2162 requested: &str,
2163 pinned: &PinnedSourceNode,
2164 maximum_bytes: u64,
2165 budget: WorkBudget,
2166 cancellation: &CancellationToken,
2167 ) -> Result<WorkCounters, LazyWorkspaceError> {
2168 let node = &pinned.node;
2169 let source = pinned.source;
2170 let mut work = WorkCounters::default();
2171 match node.kind {
2172 SourceNodeKind::Directory => {
2173 let applied = transaction
2174 .create_directory_measured(
2175 requested,
2176 remaining_work(work, budget)?,
2177 cancellation,
2178 )
2179 .await
2180 .map_err(workspace_error)?;
2181 work = account_work(work, applied, budget)?;
2182 }
2183 SourceNodeKind::RegularFile => {
2184 work = self
2185 .populate_regular_source_node(
2186 transaction,
2187 requested,
2188 pinned,
2189 maximum_bytes,
2190 budget,
2191 cancellation,
2192 )
2193 .await?;
2194 }
2195 SourceNodeKind::SymbolicLink => {
2196 let receipt = self
2197 .source
2198 .read_link(
2199 source,
2200 &self.namespace_path(requested)?,
2201 node.version,
2202 cancellation,
2203 )
2204 .await
2205 .map_err(|failure| LazyWorkspaceError::from(failure.error))?;
2206 work = account_work(work, receipt.work, budget)?;
2207 let applied = transaction
2208 .create_symbolic_link_measured(
2209 requested,
2210 receipt.value,
2211 remaining_work(work, budget)?,
2212 cancellation,
2213 )
2214 .await
2215 .map_err(workspace_error)?;
2216 work = account_work(work, applied, budget)?;
2217 }
2218 SourceNodeKind::Fifo
2219 | SourceNodeKind::Socket
2220 | SourceNodeKind::CharacterDevice
2221 | SourceNodeKind::BlockDevice => {
2222 let kind = match node.kind {
2223 SourceNodeKind::Fifo => FileKind::Fifo,
2224 SourceNodeKind::Socket => FileKind::Socket,
2225 SourceNodeKind::CharacterDevice => FileKind::CharacterDevice,
2226 SourceNodeKind::BlockDevice => FileKind::BlockDevice,
2227 _ => unreachable!(),
2228 };
2229 let applied = transaction
2230 .create_special_measured(
2231 requested,
2232 kind,
2233 node.device,
2234 remaining_work(work, budget)?,
2235 cancellation,
2236 )
2237 .await
2238 .map_err(workspace_error)?;
2239 work = account_work(work, applied, budget)?;
2240 }
2241 SourceNodeKind::Unsupported => return Err(LazyWorkspaceError::UnsupportedNode),
2242 }
2243 let applied = transaction
2244 .set_metadata_measured(
2245 requested,
2246 source_file_metadata(node.metadata),
2247 remaining_work(work, budget)?,
2248 cancellation,
2249 )
2250 .await
2251 .map_err(workspace_error)?;
2252 work = account_work(work, applied, budget)?;
2253 if node.kind != SourceNodeKind::Directory {
2254 let applied = transaction
2255 .preserve_file_identity_measured(
2256 requested,
2257 self.source_file_id(node),
2258 remaining_work(work, budget)?,
2259 cancellation,
2260 )
2261 .await
2262 .map_err(workspace_error)?;
2263 work = account_work(work, applied, budget)?;
2264 }
2265 Ok(work)
2266 }
2267
2268 async fn populate_regular_source_node(
2269 &self,
2270 transaction: &mut crate::Transaction<A, O>,
2271 requested: &str,
2272 pinned: &PinnedSourceNode,
2273 maximum_bytes: u64,
2274 budget: WorkBudget,
2275 cancellation: &CancellationToken,
2276 ) -> Result<WorkCounters, LazyWorkspaceError> {
2277 let node = &pinned.node;
2278 let source = pinned.source;
2279 let length = node
2280 .logical_bytes
2281 .ok_or(LazyWorkspaceError::NotRegularFile)?;
2282 if length > maximum_bytes {
2283 return Err(LazyWorkspaceError::TooLarge);
2284 }
2285 let applied = transaction
2286 .create_file_measured(requested, Bytes::new(), budget, cancellation)
2287 .await
2288 .map_err(workspace_error)?;
2289 let mut work = account_work(WorkCounters::default(), applied, budget)?;
2290 let mut offset = 0_u64;
2291 while offset < length {
2292 let requested_bytes = (length - offset).min(1024 * 1024);
2293 let receipt = self
2294 .source
2295 .read_range(
2296 source,
2297 &self.namespace_path(requested)?,
2298 node.version,
2299 offset,
2300 requested_bytes,
2301 cancellation,
2302 )
2303 .await
2304 .map_err(|failure| LazyWorkspaceError::from(failure.error))?;
2305 work = account_work(work, receipt.work, budget)?;
2306 let chunk = receipt.value;
2307 if chunk.is_empty() {
2308 return Err(LazyWorkspaceError::Demand(DemandError::StaleVersion));
2309 }
2310 let chunk_length =
2311 u64::try_from(chunk.len()).map_err(|_| LazyWorkspaceError::TooLarge)?;
2312 let applied = transaction
2313 .write_range_measured(
2314 requested,
2315 offset,
2316 chunk,
2317 remaining_work(work, budget)?,
2318 cancellation,
2319 )
2320 .await
2321 .map_err(workspace_error)?;
2322 work = account_work(work, applied, budget)?;
2323 offset = offset
2324 .checked_add(chunk_length)
2325 .ok_or(LazyWorkspaceError::TooLarge)?;
2326 }
2327 Ok(work)
2328 }
2329
2330 async fn stage_regular_source_content(
2331 &self,
2332 transaction: &crate::Transaction<A, O>,
2333 requested: &str,
2334 pinned: &PinnedSourceNode,
2335 budget: WorkBudget,
2336 cancellation: &CancellationToken,
2337 ) -> Result<OperationReceipt<crate::StagedContent>, LazyWorkspaceError> {
2338 let length = pinned
2339 .node
2340 .logical_bytes
2341 .ok_or(LazyWorkspaceError::NotRegularFile)?;
2342 let mut source = DemandBlobSource {
2343 provider: self.source.as_ref(),
2344 source: pinned.source,
2345 path: self.namespace_path(requested)?,
2346 version: pinned.node.version,
2347 offset: 0,
2348 length,
2349 work: None,
2350 failure: None,
2351 };
2352 let staged = transaction
2353 .stage_content_measured(&mut source, length.max(1), budget, cancellation)
2354 .await
2355 .map_err(|error| {
2356 source
2357 .failure
2358 .take()
2359 .map_or_else(|| workspace_error(error), LazyWorkspaceError::Demand)
2360 })?;
2361 if staged.value.logical_bytes() != length {
2362 return Err(LazyWorkspaceError::Demand(DemandError::StaleVersion));
2363 }
2364 Ok(staged)
2365 }
2366
2367 async fn commit_promotion(
2368 &self,
2369 mut transaction: crate::Transaction<A, O>,
2370 permit: crate::PublicationPermit,
2371 mut work: WorkCounters,
2372 budget: WorkBudget,
2373 cancellation: &CancellationToken,
2374 ) -> Result<OperationReceipt<TransactionCommit<A, O>>, LazyWorkspaceError> {
2375 let receipt = transaction
2376 .commit_with_permit_measured(permit, remaining_work(work, budget)?, cancellation)
2377 .await
2378 .map_err(workspace_error)?;
2379 work = account_work(work, receipt.work, budget)?;
2380 Ok(OperationReceipt {
2381 value: receipt.value,
2382 work,
2383 })
2384 }
2385
2386 async fn existing_promotion(
2387 &self,
2388 mut work: WorkCounters,
2389 budget: WorkBudget,
2390 cancellation: &CancellationToken,
2391 ) -> Result<OperationReceipt<TransactionCommit<A, O>>, LazyWorkspaceError> {
2392 let head = self
2393 .workspace
2394 .head_measured(remaining_work(work, budget)?, cancellation)
2395 .await
2396 .map_err(workspace_error)?;
2397 work = account_work(work, head.work, budget)?;
2398 Ok(OperationReceipt {
2399 value: TransactionCommit::AlreadyCommitted(head.value),
2400 work,
2401 })
2402 }
2403
2404 async fn commit_exactify_batch(
2405 &self,
2406 transaction: crate::Transaction<A, O>,
2407 permit: crate::PublicationPermit,
2408 budget: WorkBudget,
2409 cancellation: &CancellationToken,
2410 ) -> Result<OperationReceipt<crate::Generation<A, O>>, LazyWorkspaceError> {
2411 let receipt = self
2412 .commit_promotion(
2413 transaction,
2414 permit,
2415 WorkCounters::default(),
2416 budget,
2417 cancellation,
2418 )
2419 .await?;
2420 match receipt.value {
2421 TransactionCommit::Committed(generation)
2422 | TransactionCommit::AlreadyCommitted(generation) => Ok(OperationReceipt {
2423 value: generation,
2424 work: receipt.work,
2425 }),
2426 TransactionCommit::Conflict { .. }
2427 | TransactionCommit::Fenced
2428 | TransactionCommit::IdempotencyConflict => Err(LazyWorkspaceError::Concurrent),
2429 }
2430 }
2431
2432 #[allow(clippy::too_many_arguments)]
2433 async fn flush_exactify_source_batch(
2434 &self,
2435 transaction: &mut crate::Transaction<A, O>,
2436 pending: &mut Vec<AuthoredMutation>,
2437 pending_bytes: &mut u64,
2438 retained_bytes: &mut u64,
2439 work: WorkCounters,
2440 budget: WorkBudget,
2441 cancellation: &CancellationToken,
2442 ) -> Result<WorkCounters, LazyWorkspaceError> {
2443 if pending.is_empty() {
2444 return Ok(work);
2445 }
2446 let operations = std::mem::take(pending);
2447 let applied = transaction
2448 .apply_authored_bulk_measured(operations, remaining_work(work, budget)?, cancellation)
2449 .await
2450 .map_err(workspace_error)?;
2451 let work = account_nested_with_live_memory(work, applied, *retained_bytes, budget)?;
2452 *retained_bytes = retained_bytes
2453 .checked_sub(*pending_bytes)
2454 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
2455 *pending_bytes = 0;
2456 Ok(work)
2457 }
2458
2459 pub async fn promote_with_permit(
2461 &self,
2462 path: &str,
2463 maximum_bytes: u64,
2464 idempotency_key: IdempotencyKey,
2465 permit: crate::PublicationPermit,
2466 ) -> Result<TransactionCommit<A, O>, LazyWorkspaceError> {
2467 let cancellation = CancellationToken::new();
2468 self.promote_with_permit_measured(
2469 path,
2470 maximum_bytes,
2471 idempotency_key,
2472 permit,
2473 WorkBudget::UNBOUNDED,
2474 &cancellation,
2475 )
2476 .await
2477 .map(|receipt| receipt.value)
2478 }
2479
2480 #[allow(clippy::too_many_lines)]
2481 async fn promote_with_permit_measured(
2482 &self,
2483 path: &str,
2484 maximum_bytes: u64,
2485 idempotency_key: IdempotencyKey,
2486 permit: crate::PublicationPermit,
2487 budget: WorkBudget,
2488 cancellation: &CancellationToken,
2489 ) -> Result<OperationReceipt<TransactionCommit<A, O>>, LazyWorkspaceError> {
2490 let requested = self.canonical_path(path)?;
2491 let lookup = self
2492 .lookup_measured(&requested, budget, cancellation)
2493 .await?;
2494 self.promote_resolved_with_permit_measured(
2495 &requested,
2496 lookup.value,
2497 maximum_bytes,
2498 idempotency_key,
2499 permit,
2500 lookup.work,
2501 budget,
2502 cancellation,
2503 )
2504 .await
2505 }
2506
2507 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
2508 async fn promote_resolved_with_permit_measured(
2509 &self,
2510 requested: &str,
2511 lookup: ResolvedLazyLookup,
2512 maximum_bytes: u64,
2513 idempotency_key: IdempotencyKey,
2514 permit: crate::PublicationPermit,
2515 mut work: WorkCounters,
2516 budget: WorkBudget,
2517 cancellation: &CancellationToken,
2518 ) -> Result<OperationReceipt<TransactionCommit<A, O>>, LazyWorkspaceError> {
2519 work.verify(budget)
2520 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
2521 let transaction = self
2522 .workspace
2523 .begin_transaction_measured(
2524 idempotency_key,
2525 remaining_work(work, budget)?,
2526 cancellation,
2527 )
2528 .await
2529 .map_err(workspace_error)?;
2530 work = account_work(work, transaction.work, budget)?;
2531 let mut transaction = transaction.value;
2532 if let Some(parent) = parent_path(requested) {
2533 let applied = transaction
2534 .create_dir_all_measured(parent, remaining_work(work, budget)?, cancellation)
2535 .await
2536 .map_err(workspace_error)?;
2537 work = account_work(work, applied, budget)?;
2538 }
2539 let applied = self
2540 .apply_resolved_promotion_measured(
2541 &mut transaction,
2542 requested,
2543 lookup,
2544 maximum_bytes,
2545 remaining_work(work, budget)?,
2546 cancellation,
2547 )
2548 .await?;
2549 work = account_work(work, applied.work, budget)?;
2550 if !applied.value {
2551 return self.existing_promotion(work, budget, cancellation).await;
2552 }
2553 self.commit_promotion(transaction, permit, work, budget, cancellation)
2554 .await
2555 }
2556
2557 #[allow(clippy::too_many_arguments)]
2560 async fn apply_resolved_promotion_measured(
2561 &self,
2562 transaction: &mut crate::Transaction<A, O>,
2563 requested: &str,
2564 lookup: ResolvedLazyLookup,
2565 maximum_bytes: u64,
2566 budget: WorkBudget,
2567 cancellation: &CancellationToken,
2568 ) -> Result<OperationReceipt<bool>, LazyWorkspaceError> {
2569 let mut work = WorkCounters::default();
2570 let ResolvedLazyLookup { lookup, source } = lookup;
2571 let pinned = match lookup {
2572 LazyLookup::Source(node) => PinnedSourceNode {
2573 source: source.ok_or(LazyWorkspaceError::Concurrent)?,
2574 node,
2575 },
2576 LazyLookup::Authored {
2577 path: authored_path,
2578 ..
2579 } => {
2580 if authored_path == requested {
2581 return Ok(OperationReceipt { value: false, work });
2582 }
2583 let applied = transaction
2584 .hard_link_measured(
2585 &authored_path,
2586 requested,
2587 remaining_work(work, budget)?,
2588 cancellation,
2589 )
2590 .await
2591 .map_err(workspace_error)?;
2592 work = account_work(work, applied, budget)?;
2593 return Ok(OperationReceipt { value: true, work });
2594 }
2595 LazyLookup::Shadow { record, .. } => {
2596 let applied = transaction
2597 .restore_record_measured(
2598 requested,
2599 record,
2600 remaining_work(work, budget)?,
2601 cancellation,
2602 )
2603 .await
2604 .map_err(workspace_error)?;
2605 work = account_work(work, applied, budget)?;
2606 return Ok(OperationReceipt { value: true, work });
2607 }
2608 };
2609 let populated = self
2610 .populate_source_node_measured(
2611 transaction,
2612 requested,
2613 &pinned,
2614 maximum_bytes,
2615 remaining_work(work, budget)?,
2616 cancellation,
2617 )
2618 .await?;
2619 work = account_work(work, populated, budget)?;
2620 Ok(OperationReceipt { value: true, work })
2621 }
2622
2623 pub async fn promote_exact(
2628 &self,
2629 path: &str,
2630 expected_source: FileId,
2631 maximum_bytes: u64,
2632 idempotency_key: IdempotencyKey,
2633 ) -> Result<crate::Generation<A, O>, LazyWorkspaceError> {
2634 let requested = self.canonical_path(path)?;
2635 let lookup = self.lookup(&requested).await?;
2636 if self.direct_file_id_for_lookup(&lookup) != expected_source {
2637 return Err(LazyWorkspaceError::StaleIdentity);
2638 }
2639 if let LazyLookup::Authored { path, .. } = lookup
2640 && path == requested
2641 {
2642 return self.workspace.head().await.map_err(workspace_error);
2643 }
2644 match self
2645 .promote(&requested, maximum_bytes, idempotency_key)
2646 .await?
2647 {
2648 TransactionCommit::Committed(generation)
2649 | TransactionCommit::AlreadyCommitted(generation) => Ok(generation),
2650 TransactionCommit::Conflict { .. } | TransactionCommit::Fenced => {
2651 Err(LazyWorkspaceError::StaleIdentity)
2652 }
2653 TransactionCommit::IdempotencyConflict => Err(LazyWorkspaceError::Concurrent),
2654 }
2655 }
2656
2657 #[cfg(feature = "native-mount")]
2661 pub(crate) async fn stage_exact_into_checkout(
2662 &self,
2663 checkout: &mut crate::Checkout<A, O>,
2664 path: &str,
2665 expected_source: FileId,
2666 maximum_bytes: u64,
2667 cancellation: &CancellationToken,
2668 ) -> Result<bool, LazyWorkspaceError> {
2669 let requested = self.canonical_path(path)?;
2670 let lookup = self
2671 .lookup_measured(&requested, WorkBudget::UNBOUNDED, cancellation)
2672 .await?
2673 .value;
2674 let actual = self.direct_file_id_for_lookup(&lookup.lookup);
2675 if actual != expected_source {
2676 return Err(LazyWorkspaceError::StaleIdentity);
2677 }
2678 let candidate_path = self.namespace_path(&requested)?;
2679 if let Some(record) = checkout
2680 .lookup_no_follow(&candidate_path, WorkBudget::UNBOUNDED, cancellation)
2681 .await
2682 .map_err(|failure| workspace_error(failure.error.into()))?
2683 .value
2684 .record
2685 {
2686 return (record.file_id == expected_source)
2687 .then_some(false)
2688 .ok_or(LazyWorkspaceError::StaleIdentity);
2689 }
2690 if matches!(
2691 &lookup.lookup,
2692 LazyLookup::Authored { path, .. } if path == &requested
2693 ) {
2694 return Err(LazyWorkspaceError::StaleIdentity);
2695 }
2696 let existing_identity = if matches!(
2697 &lookup.lookup,
2698 LazyLookup::Source(node) if node.kind != SourceNodeKind::Directory
2699 ) {
2700 match checkout
2701 .read_file_record_by_id(expected_source, WorkBudget::UNBOUNDED, cancellation)
2702 .await
2703 {
2704 Ok(receipt) => Some(receipt.value),
2705 Err(failure) if matches!(failure.error, crate::FsError::NotFound) => None,
2706 Err(failure) => {
2707 return Err(LazyWorkspaceError::Workspace(failure.error.to_string()));
2708 }
2709 }
2710 } else {
2711 None
2712 };
2713 let mut transaction = crate::Transaction::for_checkout_candidate(
2714 &self.workspace,
2715 checkout.private_candidate(),
2716 );
2717 if let Some(parent) = parent_path(&requested) {
2718 transaction
2719 .create_dir_all_measured(parent, WorkBudget::UNBOUNDED, cancellation)
2720 .await
2721 .map_err(workspace_error)?;
2722 }
2723 if let Some(record) = existing_identity {
2724 transaction
2725 .restore_record_measured(&requested, record, WorkBudget::UNBOUNDED, cancellation)
2726 .await
2727 .map_err(workspace_error)?;
2728 *checkout = transaction.into_checkout_candidate();
2729 return Ok(true);
2730 }
2731 let changed = self
2732 .apply_resolved_promotion_measured(
2733 &mut transaction,
2734 &requested,
2735 lookup,
2736 maximum_bytes,
2737 WorkBudget::UNBOUNDED,
2738 cancellation,
2739 )
2740 .await?
2741 .value;
2742 if changed {
2743 *checkout = transaction.into_checkout_candidate();
2744 }
2745 Ok(changed)
2746 }
2747
2748 pub async fn list_directory(
2753 &self,
2754 path: &str,
2755 cursor: Option<LazyDirectoryCursor>,
2756 maximum_entries: u32,
2757 ) -> Result<LazyDirectoryPage, LazyWorkspaceError> {
2758 let cancellation = CancellationToken::new();
2759 self.list_directory_measured(
2760 path,
2761 cursor,
2762 maximum_entries,
2763 WorkBudget::UNBOUNDED,
2764 &cancellation,
2765 None,
2766 None,
2767 )
2768 .await
2769 .map(|receipt| receipt.value)
2770 }
2771
2772 #[cfg(feature = "native-mount")]
2775 pub(crate) async fn list_directory_in_checkout(
2776 &self,
2777 checkout: &mut crate::Checkout<A, O>,
2778 path: &str,
2779 cursor: Option<LazyDirectoryCursor>,
2780 maximum_entries: u32,
2781 mounted_mask: Option<(&[String], bool)>,
2782 ) -> Result<LazyDirectoryPage, LazyWorkspaceError> {
2783 let cancellation = CancellationToken::new();
2784 self.list_directory_measured(
2785 path,
2786 cursor,
2787 maximum_entries,
2788 WorkBudget::UNBOUNDED,
2789 &cancellation,
2790 Some(checkout),
2791 mounted_mask,
2792 )
2793 .await
2794 .map(|receipt| receipt.value)
2795 }
2796
2797 #[allow(clippy::too_many_arguments)]
2800 async fn mounted_directory_replaces_source(
2801 &self,
2802 path: &str,
2803 directory: &NamespacePath,
2804 source: SourceReference,
2805 overlay: LazyOverlayId,
2806 checkout: &mut crate::Checkout<A, O>,
2807 work: &mut WorkCounters,
2808 budget: WorkBudget,
2809 cancellation: &CancellationToken,
2810 ) -> Result<bool, LazyWorkspaceError> {
2811 let candidate = checkout
2812 .lookup_no_follow(directory, remaining_work(*work, budget)?, cancellation)
2813 .await
2814 .map_err(|failure| LazyWorkspaceError::Workspace(failure.error.to_string()))?;
2815 *work = account_work(*work, candidate.work, budget)?;
2816 let tombstoned = self
2817 .tombstoned_measured(overlay, path, remaining_work(*work, budget)?, cancellation)
2818 .await?;
2819 *work = account_work(*work, tombstoned.work, budget)?;
2820 match candidate.value.record {
2821 None if tombstoned.value => Err(LazyWorkspaceError::NotFound),
2822 Some(_) if tombstoned.value => Ok(true),
2823 Some(record) => {
2824 let source = self
2825 .source
2826 .lookup(source, directory, cancellation)
2827 .await
2828 .map_err(|failure| failure.error)?;
2829 *work = account_work(*work, source.work, budget)?;
2830 Ok(source
2831 .value
2832 .is_none_or(|node| self.source_file_id(&node) != record.file_id))
2833 }
2834 None => Ok(false),
2835 }
2836 }
2837
2838 #[allow(
2839 clippy::too_many_lines,
2840 clippy::too_many_arguments,
2841 clippy::cognitive_complexity
2842 )]
2843 async fn list_directory_measured(
2844 &self,
2845 path: &str,
2846 cursor: Option<LazyDirectoryCursor>,
2847 maximum_entries: u32,
2848 budget: WorkBudget,
2849 cancellation: &CancellationToken,
2850 mut mounted: Option<&mut crate::Checkout<A, O>>,
2851 mounted_mask: Option<(&[String], bool)>,
2852 ) -> Result<OperationReceipt<LazyDirectoryPage>, LazyWorkspaceError> {
2853 if maximum_entries == 0 {
2854 return Err(LazyWorkspaceError::InvalidPageBound);
2855 }
2856 let state = self.state_measured(budget, cancellation).await?;
2857 let mut work = state.work;
2858 let state = state.value;
2859 let directory = self.namespace_path(path)?;
2860 let mut phase = match cursor {
2861 Some(cursor)
2862 if cursor.source == state.source
2863 && cursor.overlay == state.overlay
2864 && cursor.directory == directory =>
2865 {
2866 cursor.phase
2867 }
2868 Some(_) => return Err(LazyWorkspaceError::StaleCursor),
2869 None => LazyDirectoryPhase::Source(None),
2870 };
2871 if mounted_mask.is_some_and(|(_, opaque)| opaque)
2872 && matches!(phase, LazyDirectoryPhase::Source(None))
2873 {
2874 phase = LazyDirectoryPhase::Authored(None);
2875 }
2876 if matches!(phase, LazyDirectoryPhase::Source(None))
2877 && !directory.is_root()
2878 && let Some(checkout) = mounted.as_deref_mut()
2879 && self
2880 .mounted_directory_replaces_source(
2881 path,
2882 &directory,
2883 state.source,
2884 state.overlay,
2885 checkout,
2886 &mut work,
2887 budget,
2888 cancellation,
2889 )
2890 .await?
2891 {
2892 phase = LazyDirectoryPhase::Authored(None);
2893 }
2894 let mut source_absent = false;
2895 loop {
2896 match phase {
2897 LazyDirectoryPhase::Source(source_cursor) => {
2898 let receipt = match self
2899 .source
2900 .list_page(
2901 state.source,
2902 &directory,
2903 source_cursor,
2904 maximum_entries,
2905 cancellation,
2906 )
2907 .await
2908 {
2909 Ok(receipt) => receipt,
2910 Err(failure)
2911 if mounted.is_some()
2912 && matches!(
2913 failure.error,
2914 DemandError::Absent | DemandError::NotDirectory
2915 ) =>
2916 {
2917 work = account_work(work, *failure.work, budget)?;
2918 source_absent = true;
2919 phase = LazyDirectoryPhase::Authored(None);
2920 continue;
2921 }
2922 Err(failure) => return Err(failure.error.into()),
2923 };
2924 work = account_work(work, receipt.work, budget)?;
2925 let page = receipt.value;
2926 if page.entries.len() > usize::try_from(maximum_entries).unwrap_or(usize::MAX) {
2927 return Err(LazyWorkspaceError::Work(
2928 "source directory page exceeded its requested bound".to_owned(),
2929 ));
2930 }
2931 let temporary_bytes = retained_directory_page_bytes(
2932 page.entries.capacity(),
2933 std::mem::size_of::<SourceDirectoryEntry>(),
2934 page.entries.iter().map(|entry| entry.name.retained_bytes()),
2935 )?;
2936 let mut entries = Vec::new();
2937 let projection_bytes = reserve_lazy_directory_page(
2938 &mut entries,
2939 page.entries.len(),
2940 temporary_bytes,
2941 &mut work,
2942 budget,
2943 )?;
2944 let count = page.entries.len();
2948 let child_storage = count
2949 .checked_mul(
2950 std::mem::size_of::<NamespacePath>()
2951 + std::mem::size_of::<(bool, Option<usize>)>()
2952 + 1
2953 + (directory.components().len() + 1)
2954 * std::mem::size_of::<LogicalName>(),
2955 )
2956 .and_then(|total| {
2957 page.entries.iter().try_fold(total, |sum, entry| {
2958 sum.checked_add(entry.name.retained_bytes().saturating_mul(4))
2961 })
2962 })
2963 .and_then(|total| {
2964 total.checked_add(count.saturating_mul(path.len().saturating_mul(4)))
2965 })
2966 .and_then(|bytes| u64::try_from(bytes).ok())
2967 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
2968 let live_bytes = projection_bytes
2969 .checked_add(child_storage)
2970 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
2971 work.peak_allocation_bytes = work.peak_allocation_bytes.max(live_bytes);
2972 work.verify(budget)
2973 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
2974 let mut source_paths = Vec::new();
2975 let mut slots = Vec::new();
2976 source_paths.try_reserve_exact(count).map_err(|_| {
2977 LazyWorkspaceError::Work("source page lookup allocation failed".to_owned())
2978 })?;
2979 slots.try_reserve_exact(count).map_err(|_| {
2980 LazyWorkspaceError::Work("source page lookup allocation failed".to_owned())
2981 })?;
2982 let limits = self.workspace.limits();
2983 for entry in &page.entries {
2984 let child_path = logical_child_path(path, &entry.name);
2985 if let Some(child) = child_path.as_ref() {
2986 work = account_transient_string(work, child, live_bytes, budget)?;
2987 if mounted_mask
2988 .is_some_and(|(paths, _)| paths.binary_search(child).is_ok())
2989 {
2990 slots.push((true, None));
2991 continue;
2992 }
2993 let tombstoned = self
2994 .tombstoned_measured(
2995 state.overlay,
2996 child,
2997 remaining_work(work, budget)?,
2998 cancellation,
2999 )
3000 .await?;
3001 work = account_nested_with_live_memory(
3002 work,
3003 tombstoned.work,
3004 live_bytes,
3005 budget,
3006 )?;
3007 if tombstoned.value {
3008 slots.push((true, None));
3009 continue;
3010 }
3011 let child = self.namespace_path(child)?;
3012 slots.push((false, Some(source_paths.len())));
3013 source_paths.push(child);
3014 } else {
3015 slots.push((false, None));
3016 }
3017 }
3018 let mut authored = Vec::new();
3019 authored
3020 .try_reserve_exact(source_paths.len())
3021 .map_err(|_| {
3022 LazyWorkspaceError::Work(
3023 "source page lookup allocation failed".to_owned(),
3024 )
3025 })?;
3026 authored.resize(source_paths.len(), false);
3027 if !source_paths.is_empty() {
3028 let generation = if mounted.is_some() {
3029 None
3030 } else {
3031 let head = self
3032 .workspace
3033 .head_measured(remaining_work(work, budget)?, cancellation)
3034 .await
3035 .map_err(workspace_error)?;
3036 work = account_nested_with_live_memory(
3037 work, head.work, live_bytes, budget,
3038 )?;
3039 Some(head.value)
3040 };
3041 let maximum = usize::try_from(limits.maximum_paths_per_batch)
3042 .unwrap_or(usize::MAX)
3043 .max(1);
3044 let mut offset = 0_usize;
3045 for chunk in source_paths.chunks(maximum) {
3046 let (records, lookup_work) =
3047 if let Some(checkout) = mounted.as_deref_mut() {
3048 let lookup = checkout
3049 .lookup_batch_no_follow(
3050 chunk,
3051 remaining_work(work, budget)?,
3052 cancellation,
3053 )
3054 .await
3055 .map_err(|failure| {
3056 LazyWorkspaceError::Workspace(failure.error.to_string())
3057 })?;
3058 (
3059 lookup
3060 .value
3061 .entries
3062 .into_iter()
3063 .map(|entry| entry.record)
3064 .collect::<Vec<_>>(),
3065 lookup.work,
3066 )
3067 } else {
3068 let lookup = generation
3069 .as_ref()
3070 .ok_or(LazyWorkspaceError::Concurrent)?
3071 .lookup_paths(
3072 chunk,
3073 remaining_work(work, budget)?,
3074 cancellation,
3075 )
3076 .await
3077 .map_err(|failure| {
3078 LazyWorkspaceError::Workspace(failure.error.to_string())
3079 })?;
3080 (lookup.value, lookup.work)
3081 };
3082 work = account_nested_with_live_memory(
3083 work,
3084 lookup_work,
3085 live_bytes,
3086 budget,
3087 )?;
3088 for (slot, record) in authored
3089 .get_mut(offset..offset + chunk.len())
3090 .ok_or_else(|| {
3091 LazyWorkspaceError::Work(
3092 "lookup result offset overflow".to_owned(),
3093 )
3094 })?
3095 .iter_mut()
3096 .zip(records)
3097 {
3098 *slot = record.is_some();
3099 }
3100 offset += chunk.len();
3101 }
3102 }
3103 for (entry, (hidden, index)) in page.entries.into_iter().zip(slots) {
3104 if hidden
3105 || index
3106 .is_some_and(|index| authored.get(index).copied().unwrap_or(false))
3107 {
3108 continue;
3109 }
3110 entries.push(LazyDirectoryEntry {
3111 name: entry.name,
3112 kind: entry.kind,
3113 authored: false,
3114 });
3115 }
3116 let next = Some(LazyDirectoryCursor {
3117 source: state.source,
3118 overlay: state.overlay,
3119 directory,
3120 phase: page
3121 .next
3122 .map_or(LazyDirectoryPhase::Authored(None), |next| {
3123 LazyDirectoryPhase::Source(Some(next))
3124 }),
3125 });
3126 return Ok(OperationReceipt {
3127 value: LazyDirectoryPage { entries, next },
3128 work,
3129 });
3130 }
3131 LazyDirectoryPhase::Authored(after) => {
3132 let authored_page = if let Some(checkout) = mounted {
3133 match checkout
3134 .list_directory(
3135 &directory,
3136 after.as_ref(),
3137 maximum_entries,
3138 remaining_work(work, budget)?,
3139 cancellation,
3140 )
3141 .await
3142 {
3143 Ok(page) => Ok(OperationReceipt {
3144 value: crate::WorkspaceDirectoryPage {
3145 entries: page
3146 .value
3147 .entries
3148 .into_iter()
3149 .map(|entry| crate::WorkspaceDirectoryEntry {
3150 name: entry.name,
3151 file_id: entry.file_id,
3152 kind: entry.kind,
3153 })
3154 .collect(),
3155 has_more: page.value.has_more,
3156 },
3157 work: page.work,
3158 }),
3159 Err(failure) if matches!(failure.error, crate::FsError::NotFound) => {
3160 Err(WorkspaceError::NotFound)
3161 }
3162 Err(failure) => Err(WorkspaceError::from(failure.error)),
3163 }
3164 } else {
3165 self.workspace
3166 .list_directory_measured(
3167 path,
3168 after.as_ref(),
3169 maximum_entries,
3170 remaining_work(work, budget)?,
3171 cancellation,
3172 )
3173 .await
3174 };
3175 let page = match authored_page {
3176 Ok(page) => page,
3177 Err(WorkspaceError::NotFound) if source_absent => {
3178 return Err(LazyWorkspaceError::NotFound);
3179 }
3180 Err(WorkspaceError::NotFound) => {
3181 return Ok(OperationReceipt {
3182 value: LazyDirectoryPage {
3183 entries: Vec::new(),
3184 next: None,
3185 },
3186 work,
3187 });
3188 }
3189 Err(error) => return Err(workspace_error(error)),
3190 };
3191 work = account_work(work, page.work, budget)?;
3192 let page = page.value;
3193 if page.entries.len() > usize::try_from(maximum_entries).unwrap_or(usize::MAX) {
3194 return Err(LazyWorkspaceError::Work(
3195 "authored directory page exceeded its requested bound".to_owned(),
3196 ));
3197 }
3198 let next = if page.has_more {
3199 page.entries.last().map(|entry| LazyDirectoryCursor {
3200 source: state.source,
3201 overlay: state.overlay,
3202 directory,
3203 phase: LazyDirectoryPhase::Authored(Some(entry.name.clone())),
3204 })
3205 } else {
3206 None
3207 };
3208 let temporary_bytes = retained_directory_page_bytes(
3209 page.entries.capacity(),
3210 std::mem::size_of::<crate::WorkspaceDirectoryEntry>(),
3211 page.entries.iter().map(|entry| entry.name.retained_bytes()),
3212 )?;
3213 let mut entries = Vec::new();
3214 let _projection_bytes = reserve_lazy_directory_page(
3215 &mut entries,
3216 page.entries.len(),
3217 temporary_bytes,
3218 &mut work,
3219 budget,
3220 )?;
3221 entries.extend(page.entries.into_iter().map(authored_entry));
3222 return Ok(OperationReceipt {
3223 value: LazyDirectoryPage { entries, next },
3224 work,
3225 });
3226 }
3227 }
3228 }
3229 }
3230
3231 pub async fn exactify(
3238 &self,
3239 budget: WorkBudget,
3240 cancellation: &CancellationToken,
3241 ) -> Result<OperationReceipt<crate::Generation<A, O>>, LazyWorkspaceError> {
3242 self.exactify_with_permit(budget, cancellation, crate::PublicationPermit::Unrestricted)
3243 .await
3244 }
3245
3246 #[allow(clippy::too_many_lines)]
3248 pub async fn exactify_with_permit(
3249 &self,
3250 budget: WorkBudget,
3251 cancellation: &CancellationToken,
3252 permit: crate::PublicationPermit,
3253 ) -> Result<OperationReceipt<crate::Generation<A, O>>, LazyWorkspaceError> {
3254 self.exactify_selected_with_permit("/", budget, cancellation, permit)
3255 .await
3256 }
3257
3258 pub async fn exactify_subtree(
3262 &self,
3263 path: &str,
3264 budget: WorkBudget,
3265 cancellation: &CancellationToken,
3266 ) -> Result<OperationReceipt<crate::Generation<A, O>>, LazyWorkspaceError> {
3267 self.exactify_subtree_with_permit(
3268 path,
3269 budget,
3270 cancellation,
3271 crate::PublicationPermit::Unrestricted,
3272 )
3273 .await
3274 }
3275
3276 pub async fn exactify_subtree_with_permit(
3280 &self,
3281 path: &str,
3282 budget: WorkBudget,
3283 cancellation: &CancellationToken,
3284 permit: crate::PublicationPermit,
3285 ) -> Result<OperationReceipt<crate::Generation<A, O>>, LazyWorkspaceError> {
3286 let root = self.canonical_path(path)?;
3287 let selected = self.lookup(&root).await?;
3288 if !matches!(
3289 selected,
3290 LazyLookup::Authored {
3291 stat: crate::WorkspaceStat {
3292 kind: FileKind::Directory,
3293 ..
3294 },
3295 ..
3296 } | LazyLookup::Source(SourceNode {
3297 kind: SourceNodeKind::Directory,
3298 ..
3299 }) | LazyLookup::Shadow {
3300 record: FileRecord {
3301 kind: FileKind::Directory,
3302 ..
3303 },
3304 ..
3305 }
3306 ) {
3307 return Err(LazyWorkspaceError::NotDirectory);
3308 }
3309 self.exactify_selected_with_permit(&root, budget, cancellation, permit)
3310 .await
3311 }
3312
3313 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
3314 async fn exactify_selected_with_permit(
3315 &self,
3316 root: &str,
3317 budget: WorkBudget,
3318 cancellation: &CancellationToken,
3319 permit: crate::PublicationPermit,
3320 ) -> Result<OperationReceipt<crate::Generation<A, O>>, LazyWorkspaceError> {
3321 cancellation
3322 .check()
3323 .map_err(|_| LazyWorkspaceError::Cancelled)?;
3324 let state = self.state_measured(budget, cancellation).await?;
3325 let mut work = state.work;
3326 let head = self
3327 .workspace
3328 .head_measured(remaining_work(work, budget)?, cancellation)
3329 .await
3330 .map_err(workspace_error)?;
3331 work = account_work(work, head.work, budget)?;
3332 let mut pinned_generation = head.value;
3333 let capture = LazySnapshotRef::new(
3334 self.workspace.id(),
3335 pinned_generation.id(),
3336 state.value.source,
3337 state.value.overlay,
3338 state.value.shadows,
3339 )
3340 .id;
3341 let mut retained_bytes = 0_u64;
3342 let mut directories = Vec::new();
3343 let mut paths = Vec::new();
3344 reserve_exactify_path_slot(&mut directories, &mut retained_bytes, &mut work, budget)?;
3345 let root_key = root;
3346 let root = root.to_owned();
3347 retain_exactify_string(&root, &mut retained_bytes, &mut work, budget)?;
3348 directories.push(root.clone());
3349 if root != "/" {
3350 reserve_exactify_path_slot(&mut paths, &mut retained_bytes, &mut work, budget)?;
3351 retain_exactify_string(&root, &mut retained_bytes, &mut work, budget)?;
3352 paths.push(root);
3353 }
3354 let mut next_directory = 0_usize;
3355 while next_directory < directories.len() {
3356 cancellation
3357 .check()
3358 .map_err(|_| LazyWorkspaceError::Cancelled)?;
3359 let directory = directories
3360 .get_mut(next_directory)
3361 .map(std::mem::take)
3362 .ok_or(LazyWorkspaceError::Concurrent)?;
3363 next_directory += 1;
3364 let mut cursor = None;
3365 loop {
3366 cancellation
3367 .check()
3368 .map_err(|_| LazyWorkspaceError::Cancelled)?;
3369 let receipt = self
3370 .list_directory_measured(
3371 &directory,
3372 cursor,
3373 1_024,
3374 remaining_work(work, budget)?,
3375 cancellation,
3376 None,
3377 None,
3378 )
3379 .await
3380 .map_err(|error| exactify_error("enumerate", &directory, error))?;
3381 work = account_nested_with_live_memory(work, receipt.work, retained_bytes, budget)?;
3382 let page = receipt.value;
3383 for entry in page.entries {
3384 let path = logical_child_path(&directory, &entry.name)
3385 .ok_or(LazyWorkspaceError::UnsupportedNode)?;
3386 work.items_examined = work
3387 .items_examined
3388 .checked_add(1)
3389 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3390 work.verify(budget)
3391 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
3392 if entry.kind == SourceNodeKind::Directory {
3393 reserve_exactify_path_slot(
3394 &mut directories,
3395 &mut retained_bytes,
3396 &mut work,
3397 budget,
3398 )?;
3399 let child_directory = path.clone();
3400 retain_exactify_string(
3401 &child_directory,
3402 &mut retained_bytes,
3403 &mut work,
3404 budget,
3405 )?;
3406 directories.push(child_directory);
3407 }
3408 reserve_exactify_path_slot(&mut paths, &mut retained_bytes, &mut work, budget)?;
3409 retain_exactify_string(&path, &mut retained_bytes, &mut work, budget)?;
3410 paths.push(path);
3411 }
3412 cursor = page.next;
3413 if cursor.is_none() {
3414 break;
3415 }
3416 }
3417 retained_bytes = retained_bytes
3418 .checked_sub(u64::try_from(directory.capacity()).unwrap_or(u64::MAX))
3419 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3420 }
3421 let directory_vector_bytes = exactify_path_vector_bytes(&directories)?;
3422 retained_bytes = retained_bytes
3423 .checked_sub(directory_vector_bytes)
3424 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3425 drop(directories);
3426 let path_vector_bytes = exactify_path_vector_bytes(&paths)?;
3427 paths.sort_unstable_by(|left, right| exactify_path_order(left, right));
3428 let limits = self.workspace.limits();
3429 let dependency_paths = usize::try_from(limits.maximum_checkout_dependencies)
3432 .unwrap_or(usize::MAX)
3433 / (usize::from(limits.maximum_path_depth) + 4);
3434 let mutation_paths =
3435 usize::try_from(limits.maximum_mutations_per_batch).unwrap_or(usize::MAX) / 4;
3436 let maximum_batch_paths = dependency_paths.min(mutation_paths).max(1);
3437 let mut seen_sources: Vec<(FileId, String)> = Vec::new();
3438 seen_sources
3439 .try_reserve_exact(maximum_batch_paths.min(paths.len()))
3440 .map_err(|_| LazyWorkspaceError::Work("exactify batch allocation failed".to_owned()))?;
3441 let seen_vector_bytes = u64::try_from(
3442 seen_sources
3443 .capacity()
3444 .checked_mul(std::mem::size_of::<(FileId, String)>())
3445 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
3446 )
3447 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3448 retained_bytes = retained_bytes
3449 .checked_add(seen_vector_bytes)
3450 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3451 work.peak_allocation_bytes = work.peak_allocation_bytes.max(retained_bytes);
3452 work.verify(budget)
3453 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
3454 let mut transaction = None;
3455 let mut batch_paths = 0_usize;
3456 let mut batch_dirty = false;
3457 let mut batch_index = 0_u64;
3458 let mut pending_sources = Vec::new();
3459 let mut pending_source_bytes = 0_u64;
3460 let mut authored_batch = Vec::new();
3461 for index in 0..paths.len() {
3462 cancellation
3463 .check()
3464 .map_err(|_| LazyWorkspaceError::Cancelled)?;
3465 if batch_paths == maximum_batch_paths {
3466 if batch_dirty {
3467 work = self
3468 .flush_exactify_source_batch(
3469 transaction.as_mut().ok_or(LazyWorkspaceError::Concurrent)?,
3470 &mut pending_sources,
3471 &mut pending_source_bytes,
3472 &mut retained_bytes,
3473 work,
3474 budget,
3475 cancellation,
3476 )
3477 .await?;
3478 let current = transaction.take().ok_or(LazyWorkspaceError::Concurrent)?;
3479 let committed = self
3480 .commit_exactify_batch(
3481 current,
3482 permit,
3483 remaining_work(work, budget)?,
3484 cancellation,
3485 )
3486 .await?;
3487 work = account_nested_with_live_memory(
3488 work,
3489 committed.work,
3490 retained_bytes,
3491 budget,
3492 )?;
3493 pinned_generation = committed.value;
3494 }
3495 transaction = None;
3496 batch_dirty = false;
3497 batch_paths = 0;
3498 batch_index = batch_index
3499 .checked_add(1)
3500 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3501 for (_, retained) in seen_sources.drain(..) {
3502 retained_bytes = retained_bytes
3503 .checked_sub(u64::try_from(retained.capacity()).unwrap_or(u64::MAX))
3504 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3505 }
3506 }
3507 if batch_paths == 0 {
3508 let end = index.saturating_add(maximum_batch_paths).min(paths.len());
3509 let namespace_paths = paths
3510 .get(index..end)
3511 .ok_or(LazyWorkspaceError::Concurrent)?
3512 .iter()
3513 .map(|path| self.namespace_path(path))
3514 .collect::<Result<Vec<_>, _>>()?;
3515 let found = pinned_generation
3516 .lookup_paths(
3517 &namespace_paths,
3518 remaining_work(work, budget)?,
3519 cancellation,
3520 )
3521 .await
3522 .map_err(|failure| LazyWorkspaceError::Workspace(failure.error.to_string()))?;
3523 work = account_nested_with_live_memory(work, found.work, retained_bytes, budget)?;
3524 authored_batch = found
3525 .value
3526 .into_iter()
3527 .map(|record| record.is_some())
3528 .collect();
3529 }
3530 let authored_absent = !authored_batch
3531 .get(batch_paths)
3532 .copied()
3533 .ok_or(LazyWorkspaceError::Concurrent)?;
3534 let path = std::mem::take(paths.get_mut(index).ok_or(LazyWorkspaceError::Concurrent)?);
3535 let receipt = if authored_absent {
3536 self.inspect_snapshot_after_authored_absence(
3537 &path,
3538 remaining_work(work, budget)?,
3539 cancellation,
3540 )
3541 .await
3542 } else {
3543 self.inspect_snapshot_measured(&path, remaining_work(work, budget)?, cancellation)
3544 .await
3545 }
3546 .map_err(|error| exactify_error("inspect", &path, error))?;
3547 work = account_nested_with_live_memory(work, receipt.work, retained_bytes, budget)?;
3548 let lookup = receipt.value;
3549 if matches!(lookup.lookup, LazyLookup::Source(_)) {
3550 work.materializations = work
3551 .materializations
3552 .checked_add(1)
3553 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3554 work.verify(budget)
3555 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
3556 }
3557 if transaction.is_none() {
3558 let begun = self
3559 .workspace
3560 .begin_pinned_transaction_measured(
3561 &pinned_generation,
3562 exactify_batch_key(capture, root_key, batch_index),
3563 remaining_work(work, budget)?,
3564 cancellation,
3565 )
3566 .await
3567 .map_err(workspace_error)?;
3568 work = account_nested_with_live_memory(work, begun.work, retained_bytes, budget)?;
3569 transaction = Some(begun.value);
3570 if let Some(parent) = parent_path(root_key) {
3571 let prepared = transaction
3572 .as_mut()
3573 .ok_or(LazyWorkspaceError::Concurrent)?
3574 .create_dir_all_measured(
3575 parent,
3576 remaining_work(work, budget)?,
3577 cancellation,
3578 )
3579 .await
3580 .map_err(workspace_error)?;
3581 work = account_nested_with_live_memory(work, prepared, retained_bytes, budget)?;
3582 }
3583 }
3584 let source_id = match &lookup.lookup {
3585 LazyLookup::Source(node)
3586 if node.kind == SourceNodeKind::RegularFile && node.link_count != Some(1) =>
3587 {
3588 Some(self.source_file_id(node))
3589 }
3590 _ => None,
3591 };
3592 if let Some((_, alias)) =
3593 source_id.and_then(|id| seen_sources.iter().find(|(seen, _)| *seen == id))
3594 {
3595 work = self
3596 .flush_exactify_source_batch(
3597 transaction.as_mut().ok_or(LazyWorkspaceError::Concurrent)?,
3598 &mut pending_sources,
3599 &mut pending_source_bytes,
3600 &mut retained_bytes,
3601 work,
3602 budget,
3603 cancellation,
3604 )
3605 .await?;
3606 let linked = transaction
3607 .as_mut()
3608 .ok_or(LazyWorkspaceError::Concurrent)?
3609 .hard_link_measured(alias, &path, remaining_work(work, budget)?, cancellation)
3610 .await
3611 .map_err(workspace_error)
3612 .map_err(|error| exactify_error("link", &path, error))?;
3613 work = account_nested_with_live_memory(work, linked, retained_bytes, budget)?;
3614 batch_dirty = true;
3615 retained_bytes = retained_bytes
3616 .checked_sub(u64::try_from(path.capacity()).unwrap_or(u64::MAX))
3617 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3618 } else if let LazyLookup::Source(node) = &lookup.lookup
3619 && node.kind == SourceNodeKind::RegularFile
3620 {
3621 let pinned = PinnedSourceNode {
3622 source: lookup.source.ok_or(LazyWorkspaceError::Concurrent)?,
3623 node: *node,
3624 };
3625 let staged = self
3626 .stage_regular_source_content(
3627 transaction.as_ref().ok_or(LazyWorkspaceError::Concurrent)?,
3628 &path,
3629 &pinned,
3630 remaining_work(work, budget)?,
3631 cancellation,
3632 )
3633 .await
3634 .map_err(|error| exactify_error("stage", &path, error))?;
3635 work = account_nested_with_live_memory(work, staged.work, retained_bytes, budget)?;
3636 let namespace_path = self.namespace_path(&path)?;
3637 queue_exactify_mutation(
3638 &mut pending_sources,
3639 AuthoredMutation::CreateFileFromContent {
3640 path: namespace_path,
3641 content: staged.value,
3642 metadata: source_file_metadata(node.metadata),
3643 file_id: Some(self.source_file_id(node)),
3644 },
3645 &mut pending_source_bytes,
3646 &mut retained_bytes,
3647 &mut work,
3648 budget,
3649 )?;
3650 batch_dirty = true;
3651 if let Some(id) = source_id {
3652 seen_sources.push((id, path));
3653 } else {
3654 retained_bytes = retained_bytes
3655 .checked_sub(u64::try_from(path.capacity()).unwrap_or(u64::MAX))
3656 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3657 }
3658 } else if let LazyLookup::Source(node) = &lookup.lookup
3659 && node.kind == SourceNodeKind::Directory
3660 {
3661 queue_exactify_mutation(
3662 &mut pending_sources,
3663 AuthoredMutation::CreateDirectory {
3664 path: self.namespace_path(&path)?,
3665 metadata: source_file_metadata(node.metadata),
3666 },
3667 &mut pending_source_bytes,
3668 &mut retained_bytes,
3669 &mut work,
3670 budget,
3671 )?;
3672 batch_dirty = true;
3673 retained_bytes = retained_bytes
3674 .checked_sub(u64::try_from(path.capacity()).unwrap_or(u64::MAX))
3675 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3676 } else {
3677 work = self
3678 .flush_exactify_source_batch(
3679 transaction.as_mut().ok_or(LazyWorkspaceError::Concurrent)?,
3680 &mut pending_sources,
3681 &mut pending_source_bytes,
3682 &mut retained_bytes,
3683 work,
3684 budget,
3685 cancellation,
3686 )
3687 .await?;
3688 let applied = self
3689 .apply_resolved_promotion_measured(
3690 transaction.as_mut().ok_or(LazyWorkspaceError::Concurrent)?,
3691 &path,
3692 lookup,
3693 budget.source_bytes_read,
3694 remaining_work(work, budget)?,
3695 cancellation,
3696 )
3697 .await
3698 .map_err(|error| exactify_error("promote", &path, error))?;
3699 work = account_nested_with_live_memory(work, applied.work, retained_bytes, budget)?;
3700 batch_dirty |= applied.value;
3701 if let Some(id) = source_id.filter(|_| applied.value) {
3702 seen_sources.push((id, path));
3703 } else {
3704 retained_bytes = retained_bytes
3705 .checked_sub(u64::try_from(path.capacity()).unwrap_or(u64::MAX))
3706 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3707 }
3708 }
3709 batch_paths += 1;
3710 }
3711 if batch_dirty {
3712 work = self
3713 .flush_exactify_source_batch(
3714 transaction.as_mut().ok_or(LazyWorkspaceError::Concurrent)?,
3715 &mut pending_sources,
3716 &mut pending_source_bytes,
3717 &mut retained_bytes,
3718 work,
3719 budget,
3720 cancellation,
3721 )
3722 .await?;
3723 let current = transaction.take().ok_or(LazyWorkspaceError::Concurrent)?;
3724 let committed = self
3725 .commit_exactify_batch(current, permit, remaining_work(work, budget)?, cancellation)
3726 .await?;
3727 work = account_nested_with_live_memory(work, committed.work, retained_bytes, budget)?;
3728 pinned_generation = committed.value;
3729 }
3730 for (_, retained) in seen_sources.drain(..) {
3731 retained_bytes = retained_bytes
3732 .checked_sub(u64::try_from(retained.capacity()).unwrap_or(u64::MAX))
3733 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3734 }
3735 retained_bytes = retained_bytes
3736 .checked_sub(seen_vector_bytes)
3737 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3738 drop(seen_sources);
3739 retained_bytes = retained_bytes
3740 .checked_sub(path_vector_bytes)
3741 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
3742 debug_assert_eq!(retained_bytes, 0);
3743 let head = self
3744 .workspace
3745 .head_measured(remaining_work(work, budget)?, cancellation)
3746 .await
3747 .map_err(workspace_error)
3748 .map_err(|error| exactify_error("load head", "/", error))?;
3749 work = account_work(work, head.work, budget)?;
3750 if head.value.id() != pinned_generation.id() {
3751 return Err(LazyWorkspaceError::Concurrent);
3752 }
3753 Ok(OperationReceipt {
3754 value: head.value,
3755 work,
3756 })
3757 }
3758
3759 pub async fn write(
3766 &self,
3767 path: &str,
3768 bytes: Bytes,
3769 ) -> Result<TransactionCommit<A, O>, LazyWorkspaceError> {
3770 let path = self.canonical_path(path)?;
3771 let source_identity = match self.lookup(&path).await {
3772 Ok(LazyLookup::Source(node)) => Some(self.source_file_id(&node)),
3773 Ok(LazyLookup::Authored { stat, .. }) => {
3774 self.promote_exact(&path, stat.file_id, u64::MAX, IdempotencyKey::new())
3775 .await?;
3776 None
3777 }
3778 Ok(LazyLookup::Shadow { record, .. }) => {
3779 self.promote_exact(&path, record.file_id, u64::MAX, IdempotencyKey::new())
3780 .await?;
3781 None
3782 }
3783 Err(LazyWorkspaceError::NotFound) => None,
3784 Err(error) => return Err(error),
3785 };
3786 let mut transaction = self
3787 .workspace
3788 .begin_transaction(IdempotencyKey::new())
3789 .await
3790 .map_err(workspace_error)?;
3791 if let Some(parent) = parent_path(&path) {
3792 transaction
3793 .create_dir_all(parent)
3794 .await
3795 .map_err(workspace_error)?;
3796 }
3797 transaction
3798 .write(&path, bytes)
3799 .await
3800 .map_err(workspace_error)?;
3801 if let Some(file_id) = source_identity {
3802 transaction
3803 .preserve_file_identity(&path, file_id)
3804 .await
3805 .map_err(workspace_error)?;
3806 }
3807 transaction.commit().await.map_err(workspace_error)
3808 }
3809
3810 #[cfg(feature = "native-mount")]
3814 pub(crate) async fn prepare_mount_removals(
3815 &self,
3816 paths: &[String],
3817 identity_records: &[(FileRecord, FileMetadata)],
3818 idempotency_key: IdempotencyKey,
3819 ) -> Result<(), LazyWorkspaceError> {
3820 if paths.is_empty() && identity_records.is_empty() {
3821 return Ok(());
3822 }
3823 for _ in 0..MAXIMUM_STATE_RETRIES {
3824 let state = self.state().await?;
3825 let mut overlay = state.overlay;
3826 let mut shadows = state.shadows;
3827 for path in paths {
3828 let path = self.canonical_path(path)?;
3829 overlay = self
3830 .insert_overlay(overlay, path, LazyOverlayChange::Tombstone)
3831 .await?;
3832 }
3833 for (record, metadata) in identity_records {
3834 shadows = self
3835 .insert_shadow(
3836 shadows,
3837 record.file_id,
3838 *record,
3839 crate::WorkspaceMetadata::from_engine(*metadata),
3840 )
3841 .await?;
3842 }
3843 let prepared = LazyWorkspaceState {
3844 revision: state.revision.saturating_add(1),
3845 overlay,
3846 shadows,
3847 pending_remove: Some(PendingLazyRemove {
3848 prior_overlay: state.overlay,
3849 prior_shadows: state.shadows,
3850 prepared_overlay: overlay,
3851 kind: PendingLazyRemoveKind::Mounted { idempotency_key },
3852 }),
3853 ..state.clone()
3854 };
3855 if self
3856 .store
3857 .compare_and_swap_lazy_workspace(self.workspace.id(), state.revision, prepared)
3858 .await
3859 .map_err(store_error)?
3860 {
3861 return Ok(());
3862 }
3863 }
3864 Err(LazyWorkspaceError::Concurrent)
3865 }
3866
3867 #[cfg(feature = "native-mount")]
3871 pub(crate) async fn mount_removal_publication(
3872 &self,
3873 ) -> Result<Option<bool>, LazyWorkspaceError> {
3874 let state = self
3875 .store
3876 .load_lazy_workspace(self.workspace.id())
3877 .await
3878 .map_err(store_error)?
3879 .ok_or_else(|| LazyWorkspaceError::Store("lazy binding is absent".to_owned()))?;
3880 let Some(PendingLazyRemove {
3881 kind: PendingLazyRemoveKind::Mounted { idempotency_key },
3882 ..
3883 }) = state.pending_remove
3884 else {
3885 return Ok(None);
3886 };
3887 let committed = self
3888 .workspace
3889 .operation_generation(idempotency_key)
3890 .await
3891 .map_err(workspace_error)?
3892 .is_some();
3893 Ok(Some(committed))
3894 }
3895
3896 #[cfg(feature = "native-mount")]
3898 pub(crate) async fn finish_mount_removals(&self) -> Result<bool, LazyWorkspaceError> {
3899 let Some(committed) = self.mount_removal_publication().await? else {
3900 return Ok(false);
3901 };
3902 self.recover_pending_remove().await?;
3903 Ok(committed)
3904 }
3905
3906 pub async fn remove(&self, path: &str) -> Result<(), LazyWorkspaceError> {
3908 self.remove_if(path, None).await
3909 }
3910
3911 #[allow(clippy::too_many_lines)]
3913 pub async fn remove_if(
3914 &self,
3915 path: &str,
3916 expected: Option<FileId>,
3917 ) -> Result<(), LazyWorkspaceError> {
3918 let path = self.canonical_path(path)?;
3919 let mut resolved = self.lookup(&path).await?;
3920 if let LazyLookup::Authored {
3921 path: authored_path,
3922 stat,
3923 } = &resolved
3924 && authored_path != &path
3925 {
3926 self.promote_exact(&path, stat.file_id, u64::MAX, IdempotencyKey::new())
3927 .await?;
3928 resolved = self.lookup(&path).await?;
3929 }
3930 let actual = self.direct_file_id_for_lookup(&resolved);
3931 if let Some(expected) = expected
3932 && actual != expected
3933 {
3934 return Err(LazyWorkspaceError::StaleIdentity);
3935 }
3936 let removal_kind = if matches!(resolved, LazyLookup::Authored { .. }) {
3937 PendingLazyRemoveKind::Authored {
3938 idempotency_key: IdempotencyKey::new(),
3939 }
3940 } else {
3941 PendingLazyRemoveKind::SourceOnly
3942 };
3943 let mut prepared = None;
3944 for _ in 0..MAXIMUM_STATE_RETRIES {
3945 let state = self.state().await?;
3946 let shadows = if let LazyLookup::Authored { stat, .. } = &resolved {
3947 let record = self
3948 .workspace
3949 .record_by_id(stat.file_id)
3950 .await
3951 .map_err(workspace_error)?;
3952 self.insert_shadow(state.shadows, stat.file_id, record, stat.metadata)
3953 .await?
3954 } else {
3955 state.shadows
3956 };
3957 let tombstone_overlay = self
3958 .insert_overlay(state.overlay, path.clone(), LazyOverlayChange::Tombstone)
3959 .await?;
3960 let pending_remove = PendingLazyRemove {
3961 prior_overlay: state.overlay,
3962 prior_shadows: state.shadows,
3963 prepared_overlay: tombstone_overlay,
3964 kind: removal_kind,
3965 };
3966 let replacement = LazyWorkspaceState {
3967 revision: state.revision.saturating_add(1),
3968 overlay: tombstone_overlay,
3969 shadows,
3970 pending_remove: Some(pending_remove),
3971 ..state.clone()
3972 };
3973 if self
3974 .store
3975 .compare_and_swap_lazy_workspace(
3976 self.workspace.id(),
3977 state.revision,
3978 replacement.clone(),
3979 )
3980 .await
3981 .map_err(store_error)?
3982 {
3983 prepared = Some(replacement);
3984 break;
3985 }
3986 }
3987 let prepared = prepared.ok_or(LazyWorkspaceError::Concurrent)?;
3988
3989 let removal = if let PendingLazyRemoveKind::Authored { idempotency_key } = removal_kind {
3990 let mut transaction = self
3991 .workspace
3992 .begin_transaction(idempotency_key)
3993 .await
3994 .map_err(workspace_error)?;
3995 match transaction.remove_if(&path, actual).await {
3996 Ok(()) => transaction.commit().await,
3997 Err(error) => Err(error),
3998 }
3999 } else {
4000 Ok(TransactionCommit::AlreadyCommitted(
4001 self.workspace.head().await.map_err(workspace_error)?,
4002 ))
4003 };
4004 let prior_overlay = prepared
4005 .pending_remove
4006 .as_ref()
4007 .ok_or(LazyWorkspaceError::Concurrent)?
4008 .prior_overlay;
4009 let (overlay, result) = match removal {
4010 Ok(TransactionCommit::Committed(_))
4011 | Ok(TransactionCommit::AlreadyCommitted(_))
4012 | Err(WorkspaceError::NotFound) => (prepared.overlay, Ok(())),
4013 Ok(TransactionCommit::Conflict { .. }) | Ok(TransactionCommit::Fenced) => {
4014 (prior_overlay, Err(LazyWorkspaceError::StaleIdentity))
4015 }
4016 Ok(TransactionCommit::IdempotencyConflict) => {
4017 (prior_overlay, Err(LazyWorkspaceError::Concurrent))
4018 }
4019 Err(error) => (prior_overlay, Err(workspace_error(error))),
4020 };
4021 let replacement = LazyWorkspaceState {
4022 revision: prepared.revision.saturating_add(1),
4023 overlay,
4024 shadows: if result.is_ok() {
4025 prepared.shadows
4026 } else {
4027 prepared
4028 .pending_remove
4029 .as_ref()
4030 .ok_or(LazyWorkspaceError::Concurrent)?
4031 .prior_shadows
4032 },
4033 pending_remove: None,
4034 ..prepared.clone()
4035 };
4036 if !self
4037 .store
4038 .compare_and_swap_lazy_workspace(self.workspace.id(), prepared.revision, replacement)
4039 .await
4040 .map_err(store_error)?
4041 {
4042 return Err(LazyWorkspaceError::Concurrent);
4043 }
4044 result
4045 }
4046
4047 async fn state(&self) -> Result<LazyWorkspaceState, LazyWorkspaceError> {
4048 let state = self
4049 .store
4050 .load_lazy_workspace(self.workspace.id())
4051 .await
4052 .map_err(store_error)?
4053 .ok_or_else(|| LazyWorkspaceError::Store("lazy binding is absent".to_owned()))?;
4054 if state.schema_version != LAZY_STATE_SCHEMA
4055 || state.workspace_id != self.workspace.id()
4056 || state.source != self.source.reference()
4057 {
4058 return Err(LazyWorkspaceError::StaleSource);
4059 }
4060 if state.pending_remove.is_some() {
4061 return Err(LazyWorkspaceError::Concurrent);
4062 }
4063 Ok(state)
4064 }
4065
4066 async fn state_measured(
4067 &self,
4068 budget: WorkBudget,
4069 cancellation: &CancellationToken,
4070 ) -> Result<OperationReceipt<LazyWorkspaceState>, LazyWorkspaceError> {
4071 let receipt = self
4072 .store
4073 .load_lazy_workspace_measured(self.workspace.id(), budget, cancellation)
4074 .await?;
4075 let state = receipt
4076 .value
4077 .ok_or_else(|| LazyWorkspaceError::Store("lazy binding is absent".to_owned()))?;
4078 if state.schema_version != LAZY_STATE_SCHEMA
4079 || state.workspace_id != self.workspace.id()
4080 || state.source != self.source.reference()
4081 {
4082 return Err(LazyWorkspaceError::StaleSource);
4083 }
4084 if state.pending_remove.is_some() {
4085 return Err(LazyWorkspaceError::Concurrent);
4086 }
4087 Ok(OperationReceipt {
4088 value: state,
4089 work: receipt.work,
4090 })
4091 }
4092
4093 async fn recover_pending_remove(&self) -> Result<(), LazyWorkspaceError> {
4094 for _ in 0..MAXIMUM_STATE_RETRIES {
4095 let state = self
4096 .store
4097 .load_lazy_workspace(self.workspace.id())
4098 .await
4099 .map_err(store_error)?
4100 .ok_or_else(|| LazyWorkspaceError::Store("lazy binding is absent".to_owned()))?;
4101 if state.schema_version != LAZY_STATE_SCHEMA
4102 || state.workspace_id != self.workspace.id()
4103 || state.source.identity != self.source.reference().identity
4104 {
4105 return Err(LazyWorkspaceError::StaleSource);
4106 }
4107 let Some(pending) = state.pending_remove.as_ref() else {
4108 return Ok(());
4109 };
4110 let committed = match pending.kind {
4111 PendingLazyRemoveKind::SourceOnly => true,
4112 PendingLazyRemoveKind::Authored { idempotency_key }
4113 | PendingLazyRemoveKind::Mounted { idempotency_key } => self
4114 .workspace
4115 .operation_generation(idempotency_key)
4116 .await
4117 .map_err(workspace_error)?
4118 .is_some(),
4119 };
4120 let replacement = LazyWorkspaceState {
4121 revision: state.revision.saturating_add(1),
4122 overlay: if committed {
4123 pending.prepared_overlay
4124 } else {
4125 pending.prior_overlay
4126 },
4127 shadows: if committed {
4128 state.shadows
4129 } else {
4130 pending.prior_shadows
4131 },
4132 pending_remove: None,
4133 ..state.clone()
4134 };
4135 if self
4136 .store
4137 .compare_and_swap_lazy_workspace(self.workspace.id(), state.revision, replacement)
4138 .await
4139 .map_err(store_error)?
4140 {
4141 return Ok(());
4142 }
4143 }
4144 Err(LazyWorkspaceError::Concurrent)
4145 }
4146
4147 async fn observe_measured(
4148 &self,
4149 path: &str,
4150 state: LazyWorkspaceState,
4151 cancellation: &CancellationToken,
4152 ) -> Result<OperationReceipt<(SourceReference, SourceNode)>, LazyWorkspaceError> {
4153 let path = self.canonical_path(path)?;
4154 if let Some(change) = self.overlay_fact(state.overlay, &path).await? {
4155 return match change {
4156 LazyOverlayChange::Observe { source, node } if source == state.source => {
4157 Ok(OperationReceipt {
4158 value: (source, node),
4159 work: WorkCounters::default(),
4160 })
4161 }
4162 LazyOverlayChange::Observe { .. } => {
4163 self.observe_live_measured(path, state, cancellation).await
4164 }
4165 LazyOverlayChange::Tombstone => Err(LazyWorkspaceError::NotFound),
4166 };
4167 }
4168 self.observe_live_measured(path, state, cancellation).await
4169 }
4170
4171 async fn observe_live_measured(
4172 &self,
4173 path: String,
4174 state: LazyWorkspaceState,
4175 cancellation: &CancellationToken,
4176 ) -> Result<OperationReceipt<(SourceReference, SourceNode)>, LazyWorkspaceError> {
4177 let receipt = self
4178 .source
4179 .lookup(state.source, &self.namespace_path(&path)?, cancellation)
4180 .await
4181 .map_err(|failure| failure.error)?;
4182 let node = receipt.value.ok_or(LazyWorkspaceError::NotFound)?;
4183 self.append_overlay(
4184 path,
4185 LazyOverlayChange::Observe {
4186 source: state.source,
4187 node,
4188 },
4189 )
4190 .await?;
4191 Ok(OperationReceipt {
4192 value: (state.source, node),
4193 work: receipt.work,
4194 })
4195 }
4196
4197 async fn append_overlay(
4198 &self,
4199 path: String,
4200 change: LazyOverlayChange,
4201 ) -> Result<(), LazyWorkspaceError> {
4202 for _ in 0..MAXIMUM_STATE_RETRIES {
4203 let state = self.state().await?;
4204 let overlay_id = self
4205 .insert_overlay(state.overlay, path.clone(), change.clone())
4206 .await?;
4207 let replacement = LazyWorkspaceState {
4208 revision: state.revision.saturating_add(1),
4209 overlay: overlay_id,
4210 ..state.clone()
4211 };
4212 if self
4213 .store
4214 .compare_and_swap_lazy_workspace(self.workspace.id(), state.revision, replacement)
4215 .await
4216 .map_err(store_error)?
4217 {
4218 return Ok(());
4219 }
4220 }
4221 Err(LazyWorkspaceError::Concurrent)
4222 }
4223
4224 fn insert_overlay<'a>(
4225 &'a self,
4226 root: LazyOverlayId,
4227 path: String,
4228 change: LazyOverlayChange,
4229 ) -> LazyFuture<'a, Result<LazyOverlayId, LazyWorkspaceError>> {
4230 Box::pin(async move {
4231 insert_immutable_treap(&OverlayTreap(&self.store), root, path, change).await
4232 })
4233 }
4234
4235 async fn overlay_fact(
4236 &self,
4237 root: LazyOverlayId,
4238 path: &str,
4239 ) -> Result<Option<LazyOverlayChange>, LazyWorkspaceError> {
4240 immutable_treap_value(&OverlayTreap(&self.store), root, &path.to_owned()).await
4241 }
4242
4243 async fn overlay_fact_measured(
4244 &self,
4245 mut root: LazyOverlayId,
4246 path: &str,
4247 budget: WorkBudget,
4248 cancellation: &CancellationToken,
4249 ) -> Result<OperationReceipt<Option<LazyOverlayChange>>, LazyWorkspaceError> {
4250 let mut work = WorkCounters::default();
4251 loop {
4252 let receipt = self
4253 .store
4254 .load_lazy_overlay_measured(root, remaining_work(work, budget)?, cancellation)
4255 .await?;
4256 work = account_work(work, receipt.work, budget)?;
4257 let Some(node) = receipt.value else {
4258 return Ok(OperationReceipt { value: None, work });
4259 };
4260 let LazyOverlay::Node {
4261 path: node_path,
4262 change,
4263 left,
4264 right,
4265 ..
4266 } = node
4267 else {
4268 return Ok(OperationReceipt { value: None, work });
4269 };
4270 match path.cmp(node_path.as_str()) {
4271 std::cmp::Ordering::Equal => {
4272 return Ok(OperationReceipt {
4273 value: Some(change),
4274 work,
4275 });
4276 }
4277 std::cmp::Ordering::Less => root = left,
4278 std::cmp::Ordering::Greater => root = right,
4279 }
4280 }
4281 }
4282
4283 async fn shadow_record_measured(
4284 &self,
4285 mut root: LazyShadowId,
4286 file_id: FileId,
4287 budget: WorkBudget,
4288 cancellation: &CancellationToken,
4289 ) -> Result<OperationReceipt<Option<(FileRecord, crate::WorkspaceMetadata)>>, LazyWorkspaceError>
4290 {
4291 let mut work = WorkCounters::default();
4292 loop {
4293 let receipt = self
4294 .store
4295 .load_lazy_shadow_measured(root, remaining_work(work, budget)?, cancellation)
4296 .await?;
4297 work = account_work(work, receipt.work, budget)?;
4298 let Some(node) = receipt.value else {
4299 return Ok(OperationReceipt { value: None, work });
4300 };
4301 let LazyShadow::Node {
4302 file_id: node_file_id,
4303 record,
4304 metadata,
4305 left,
4306 right,
4307 ..
4308 } = node
4309 else {
4310 return Ok(OperationReceipt { value: None, work });
4311 };
4312 match file_id.cmp(&node_file_id) {
4313 std::cmp::Ordering::Equal => {
4314 let record = crate::kernel::decode_file_record(&record)
4315 .map_err(|error| LazyWorkspaceError::Store(error.to_string()))?;
4316 return Ok(OperationReceipt {
4317 value: Some((record, *metadata)),
4318 work,
4319 });
4320 }
4321 std::cmp::Ordering::Less => root = left,
4322 std::cmp::Ordering::Greater => root = right,
4323 }
4324 }
4325 }
4326
4327 fn insert_shadow<'a>(
4328 &'a self,
4329 root: LazyShadowId,
4330 file_id: FileId,
4331 record: FileRecord,
4332 metadata: crate::WorkspaceMetadata,
4333 ) -> LazyFuture<'a, Result<LazyShadowId, LazyWorkspaceError>> {
4334 Box::pin(async move {
4335 insert_immutable_treap(
4336 &ShadowTreap(&self.store),
4337 root,
4338 file_id,
4339 ShadowValue {
4340 record: crate::kernel::encode_file_record(record),
4341 metadata: Box::new(metadata),
4342 },
4343 )
4344 .await
4345 })
4346 }
4347
4348 async fn tombstoned_measured(
4349 &self,
4350 overlay_id: LazyOverlayId,
4351 path: &str,
4352 budget: WorkBudget,
4353 cancellation: &CancellationToken,
4354 ) -> Result<OperationReceipt<bool>, LazyWorkspaceError> {
4355 let path = self.canonical_path(path)?;
4356 let path_bytes = u64::try_from(path.capacity())
4357 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4358 let mut work = WorkCounters {
4359 bytes_copied: u64::try_from(path.len())
4360 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
4361 allocation_operations: u64::from(path_bytes != 0),
4362 peak_allocation_bytes: path_bytes,
4363 ..WorkCounters::default()
4364 };
4365 work.verify(budget)
4366 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4367 let ancestor_request = path.len();
4368 if ancestor_request != 0 {
4369 let requested = u64::try_from(ancestor_request)
4370 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4371 let mut attempted = work
4372 .checked_add(WorkCounters {
4373 bytes_copied: requested,
4374 allocation_operations: 1,
4375 ..WorkCounters::default()
4376 })
4377 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4378 attempted.peak_allocation_bytes = attempted.peak_allocation_bytes.max(
4379 path_bytes
4380 .checked_add(requested)
4381 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
4382 );
4383 attempted
4384 .verify(budget)
4385 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4386 work = attempted;
4387 }
4388 let root = self
4389 .overlay_fact_measured(overlay_id, "/", remaining_work(work, budget)?, cancellation)
4390 .await?;
4391 work = account_work(work, root.work, budget)?;
4392 if matches!(root.value, Some(LazyOverlayChange::Tombstone)) {
4393 return Ok(OperationReceipt { value: true, work });
4394 }
4395 let mut ancestor = String::new();
4396 ancestor
4397 .try_reserve_exact(ancestor_request)
4398 .map_err(|_| LazyWorkspaceError::Work("tombstone path allocation failed".to_owned()))?;
4399 for component in path.trim_start_matches('/').split('/') {
4400 if component.is_empty() {
4401 continue;
4402 }
4403 ancestor.push('/');
4404 ancestor.push_str(component);
4405 let fact = self
4406 .overlay_fact_measured(
4407 overlay_id,
4408 &ancestor,
4409 remaining_work(work, budget)?,
4410 cancellation,
4411 )
4412 .await?;
4413 work = account_work(work, fact.work, budget)?;
4414 if matches!(fact.value, Some(LazyOverlayChange::Tombstone)) {
4415 return Ok(OperationReceipt { value: true, work });
4416 }
4417 }
4418 Ok(OperationReceipt { value: false, work })
4419 }
4420
4421 fn canonical_path(&self, path: &str) -> Result<String, LazyWorkspaceError> {
4422 PortablePath::parse(path, self.workspace.limits())
4423 .map(|path| path.as_str().to_owned())
4424 .map_err(|error| LazyWorkspaceError::Workspace(error.to_string()))
4425 }
4426
4427 pub(crate) fn namespace_path(&self, path: &str) -> Result<NamespacePath, LazyWorkspaceError> {
4428 let portable = PortablePath::parse(path, self.workspace.limits())
4429 .map_err(|error| LazyWorkspaceError::Workspace(error.to_string()))?;
4430 NamespacePath::from_portable_in_profile(
4431 &portable,
4432 self.workspace.profile(),
4433 self.workspace.limits(),
4434 )
4435 .map_err(|error| LazyWorkspaceError::Workspace(error.to_string()))
4436 }
4437}
4438
4439fn store_error(error: impl std::fmt::Display) -> LazyWorkspaceError {
4440 LazyWorkspaceError::Store(error.to_string())
4441}
4442
4443fn lazy_store_read_work() -> WorkCounters {
4444 WorkCounters {
4445 object_probes: 1,
4446 backend_read_operations: 1,
4447 items_examined: 1,
4448 ..WorkCounters::default()
4449 }
4450}
4451
4452fn lazy_store_write_work() -> WorkCounters {
4453 WorkCounters {
4454 backend_write_operations: 1,
4455 items_examined: 1,
4456 ..WorkCounters::default()
4457 }
4458}
4459
4460fn measured_lazy_store_read(
4461 budget: WorkBudget,
4462 cancellation: &CancellationToken,
4463) -> Result<(), LazyWorkspaceError> {
4464 cancellation
4465 .check()
4466 .map_err(|_| LazyWorkspaceError::Cancelled)?;
4467 lazy_store_read_work()
4468 .verify(budget)
4469 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))
4470}
4471
4472fn measured_lazy_store_write(
4473 budget: WorkBudget,
4474 cancellation: &CancellationToken,
4475) -> Result<(), LazyWorkspaceError> {
4476 cancellation
4477 .check()
4478 .map_err(|_| LazyWorkspaceError::Cancelled)?;
4479 lazy_store_write_work()
4480 .verify(budget)
4481 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))
4482}
4483
4484fn workspace_error(error: WorkspaceError) -> LazyWorkspaceError {
4485 match error {
4486 WorkspaceError::StaleIdentity => LazyWorkspaceError::StaleIdentity,
4487 WorkspaceError::Cancelled(_) => LazyWorkspaceError::Cancelled,
4488 WorkspaceError::Work(error) => LazyWorkspaceError::Work(error.to_string()),
4489 error => LazyWorkspaceError::Workspace(error.to_string()),
4490 }
4491}
4492
4493fn source_kind(kind: crate::kernel::FileKind) -> SourceNodeKind {
4494 match kind {
4495 crate::kernel::FileKind::Directory => SourceNodeKind::Directory,
4496 crate::kernel::FileKind::Regular => SourceNodeKind::RegularFile,
4497 crate::kernel::FileKind::SymbolicLink => SourceNodeKind::SymbolicLink,
4498 crate::kernel::FileKind::Fifo => SourceNodeKind::Fifo,
4499 crate::kernel::FileKind::Socket => SourceNodeKind::Socket,
4500 crate::kernel::FileKind::CharacterDevice => SourceNodeKind::CharacterDevice,
4501 crate::kernel::FileKind::BlockDevice => SourceNodeKind::BlockDevice,
4502 crate::kernel::FileKind::ReparsePoint | crate::kernel::FileKind::MountBoundary => {
4503 SourceNodeKind::Unsupported
4504 }
4505 }
4506}
4507
4508fn record_logical_bytes(record: FileRecord) -> Option<u64> {
4509 match record.payload {
4510 FilePayload::InlineRegular(bytes) => u64::try_from(bytes.as_bytes().len()).ok(),
4511 FilePayload::Regular { logical_bytes, .. }
4512 | FilePayload::SymbolicLink {
4513 target_bytes: logical_bytes,
4514 ..
4515 }
4516 | FilePayload::ReparsePoint {
4517 payload_bytes: logical_bytes,
4518 ..
4519 } => Some(logical_bytes),
4520 FilePayload::Directory { .. } | FilePayload::Empty | FilePayload::Device { .. } => None,
4521 }
4522}
4523
4524fn source_file_metadata(source: crate::demand::SourceMetadata) -> FileMetadata {
4525 let time = |value: Option<i128>| {
4526 value
4527 .and_then(|value| i64::try_from(value).ok())
4528 .map_or(MetadataField::Unavailable, MetadataField::Value)
4529 };
4530 FileMetadata {
4531 posix_mode: metadata_u32(source.posix_mode),
4532 posix_uid: metadata_u32(source.posix_uid),
4533 posix_gid: metadata_u32(source.posix_gid),
4534 posix_flags: metadata_u64(source.posix_flags),
4535 windows_attributes: metadata_u32(source.windows_attributes),
4536 created_ns: time(source.created_ns),
4537 modified_ns: time(source.modified_ns),
4538 accessed_ns: time(source.accessed_ns),
4539 changed_ns: time(source.changed_ns),
4540 ..FileMetadata::default()
4541 }
4542}
4543
4544fn metadata_u32(value: Option<u32>) -> MetadataField<u32> {
4545 value.map_or(MetadataField::Unavailable, MetadataField::Value)
4546}
4547
4548fn metadata_u64(value: Option<u64>) -> MetadataField<u64> {
4549 value.map_or(MetadataField::Unavailable, MetadataField::Value)
4550}
4551
4552fn path_priority(path: &str) -> u64 {
4553 let digest = blake3::hash(path.as_bytes());
4554 let mut bytes = [0_u8; 8];
4555 if let Some(prefix) = digest.as_bytes().get(..8) {
4556 bytes.copy_from_slice(prefix);
4557 }
4558 u64::from_be_bytes(bytes)
4559}
4560
4561fn shadow_priority(file_id: FileId) -> u64 {
4562 let digest = blake3::hash(&file_id.into_bytes());
4563 let mut bytes = [0_u8; 8];
4564 bytes.copy_from_slice(&digest.as_bytes()[..8]);
4565 u64::from_le_bytes(bytes)
4566}
4567
4568fn parent_path(path: &str) -> Option<&str> {
4569 let index = path.rfind('/')?;
4570 if index == 0 {
4571 Some("/")
4572 } else {
4573 path.get(..index)
4574 }
4575}
4576
4577fn authored_entry(entry: WorkspaceDirectoryEntry) -> LazyDirectoryEntry {
4578 LazyDirectoryEntry {
4579 name: entry.name,
4580 kind: source_kind(entry.kind),
4581 authored: true,
4582 }
4583}
4584
4585fn logical_child_path(parent: &str, name: &LogicalName) -> Option<String> {
4586 let component = match name.encoding() {
4587 NameEncoding::Utf8 | NameEncoding::PosixBytes => {
4588 std::str::from_utf8(name.as_bytes()).ok()?.to_owned()
4589 }
4590 NameEncoding::WindowsUtf16Le => {
4591 let units = name
4592 .as_bytes()
4593 .chunks_exact(2)
4594 .filter_map(|pair| pair.try_into().ok().map(u16::from_le_bytes));
4595 char::decode_utf16(units)
4596 .collect::<Result<String, _>>()
4597 .ok()?
4598 }
4599 };
4600 Some(if parent == "/" {
4601 format!("/{component}")
4602 } else {
4603 format!("{parent}/{component}")
4604 })
4605}
4606
4607fn exactify_path_order(left: &str, right: &str) -> std::cmp::Ordering {
4608 left.matches('/')
4609 .count()
4610 .cmp(&right.matches('/').count())
4611 .then_with(|| left.cmp(right))
4612}
4613
4614fn account_work(
4615 current: WorkCounters,
4616 additional: WorkCounters,
4617 budget: WorkBudget,
4618) -> Result<WorkCounters, LazyWorkspaceError> {
4619 let combined = current
4620 .checked_add(additional)
4621 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4622 combined
4623 .verify(budget)
4624 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4625 Ok(combined)
4626}
4627
4628fn account_nested_with_live_memory(
4629 current: WorkCounters,
4630 mut nested: WorkCounters,
4631 live_bytes: u64,
4632 budget: WorkBudget,
4633) -> Result<WorkCounters, LazyWorkspaceError> {
4634 let simultaneous_peak = live_bytes
4635 .checked_add(nested.peak_allocation_bytes)
4636 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4637 nested.peak_allocation_bytes = 0;
4638 let mut combined = current
4639 .checked_add(nested)
4640 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4641 combined.peak_allocation_bytes = combined.peak_allocation_bytes.max(simultaneous_peak);
4642 combined
4643 .verify(budget)
4644 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4645 Ok(combined)
4646}
4647
4648#[allow(clippy::too_many_arguments)]
4649fn queue_exactify_mutation(
4650 pending: &mut Vec<AuthoredMutation>,
4651 mutation: AuthoredMutation,
4652 pending_bytes: &mut u64,
4653 retained_bytes: &mut u64,
4654 work: &mut WorkCounters,
4655 budget: WorkBudget,
4656) -> Result<(), LazyWorkspaceError> {
4657 let (AuthoredMutation::CreateFileFromContent { path, .. }
4658 | AuthoredMutation::CreateDirectory { path, .. }) = &mutation
4659 else {
4660 return Err(LazyWorkspaceError::UnsupportedNode);
4661 };
4662 let name_bytes = path.components().iter().try_fold(0_u64, |sum, name| {
4663 sum.checked_add(u64::try_from(name.retained_bytes()).ok()?)
4664 });
4665 let path_bytes = u64::try_from(path.components().len())
4666 .ok()
4667 .and_then(|count| {
4668 count.checked_mul(u64::try_from(std::mem::size_of::<LogicalName>()).ok()?)
4669 })
4670 .and_then(|bytes| bytes.checked_add(name_bytes?))
4671 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4672 let item_bytes = u64::try_from(std::mem::size_of::<AuthoredMutation>())
4673 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4674 let growth = u64::from(pending.len() == pending.capacity())
4675 .checked_mul(item_bytes)
4676 .and_then(|bytes| bytes.checked_add(path_bytes))
4677 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4678 let prospective = retained_bytes
4679 .checked_add(growth)
4680 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4681 work.peak_allocation_bytes = work.peak_allocation_bytes.max(prospective);
4682 work.verify(budget)
4683 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4684 let old_capacity = pending.capacity();
4685 pending
4686 .try_reserve_exact(1)
4687 .map_err(|_| LazyWorkspaceError::Work("exactify mutation allocation failed".to_owned()))?;
4688 let new_items = pending.capacity().saturating_sub(old_capacity);
4689 let actual_growth = u64::try_from(new_items)
4690 .ok()
4691 .and_then(|items| items.checked_mul(item_bytes))
4692 .and_then(|bytes| bytes.checked_add(path_bytes))
4693 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4694 *pending_bytes = pending_bytes
4695 .checked_add(actual_growth)
4696 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4697 *retained_bytes = retained_bytes
4698 .checked_add(actual_growth)
4699 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4700 work.peak_allocation_bytes = work.peak_allocation_bytes.max(*retained_bytes);
4701 work.verify(budget)
4702 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4703 pending.push(mutation);
4704 Ok(())
4705}
4706
4707fn reserve_exactify_path_slot(
4708 paths: &mut Vec<String>,
4709 retained_bytes: &mut u64,
4710 work: &mut WorkCounters,
4711 budget: WorkBudget,
4712) -> Result<(), LazyWorkspaceError> {
4713 if paths.len() < paths.capacity() {
4714 return Ok(());
4715 }
4716 let item_bytes = u64::try_from(std::mem::size_of::<String>())
4717 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4718 let old_bytes = u64::try_from(paths.capacity())
4719 .ok()
4720 .and_then(|count| count.checked_mul(item_bytes))
4721 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4722 let target = paths
4723 .len()
4724 .checked_add(1)
4725 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4726 let requested_bytes = u64::try_from(target)
4727 .ok()
4728 .and_then(|count| count.checked_mul(item_bytes))
4729 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4730 let copied = u64::try_from(paths.len())
4731 .ok()
4732 .and_then(|count| count.checked_mul(item_bytes))
4733 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4734 let mut attempted = work
4735 .checked_add(WorkCounters {
4736 bytes_copied: copied,
4737 allocation_operations: 1,
4738 ..WorkCounters::default()
4739 })
4740 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4741 attempted.peak_allocation_bytes = attempted.peak_allocation_bytes.max(
4742 retained_bytes
4743 .checked_add(requested_bytes)
4744 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
4745 );
4746 attempted
4747 .verify(budget)
4748 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4749 paths
4750 .try_reserve_exact(1)
4751 .map_err(|_| LazyWorkspaceError::Work("exactify path allocation failed".to_owned()))?;
4752 let new_bytes = u64::try_from(paths.capacity())
4753 .ok()
4754 .and_then(|count| count.checked_mul(item_bytes))
4755 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4756 *retained_bytes = retained_bytes
4757 .checked_sub(old_bytes)
4758 .and_then(|value| value.checked_add(new_bytes))
4759 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4760 attempted.peak_allocation_bytes = attempted.peak_allocation_bytes.max(*retained_bytes);
4761 attempted
4762 .verify(budget)
4763 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4764 *work = attempted;
4765 Ok(())
4766}
4767
4768fn account_transient_string(
4769 work: WorkCounters,
4770 value: &String,
4771 live_bytes: u64,
4772 budget: WorkBudget,
4773) -> Result<WorkCounters, LazyWorkspaceError> {
4774 let bytes = u64::try_from(value.capacity())
4775 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4776 let copied = u64::try_from(value.len())
4777 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4778 let mut delta = WorkCounters {
4779 bytes_copied: copied,
4780 allocation_operations: u64::from(bytes != 0),
4781 ..WorkCounters::default()
4782 };
4783 delta.peak_allocation_bytes = live_bytes
4784 .checked_add(bytes)
4785 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4786 account_work(work, delta, budget)
4787}
4788
4789fn retained_directory_page_bytes(
4790 capacity: usize,
4791 entry_bytes: usize,
4792 mut names: impl Iterator<Item = usize>,
4793) -> Result<u64, LazyWorkspaceError> {
4794 let slots = capacity
4795 .checked_mul(entry_bytes)
4796 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4797 let names = names
4798 .try_fold(0_usize, |total, bytes| total.checked_add(bytes))
4799 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4800 u64::try_from(
4801 slots
4802 .checked_add(names)
4803 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
4804 )
4805 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))
4806}
4807
4808fn reserve_lazy_directory_page(
4809 entries: &mut Vec<LazyDirectoryEntry>,
4810 count: usize,
4811 temporary_bytes: u64,
4812 work: &mut WorkCounters,
4813 budget: WorkBudget,
4814) -> Result<u64, LazyWorkspaceError> {
4815 let requested = count
4816 .checked_mul(std::mem::size_of::<LazyDirectoryEntry>())
4817 .and_then(|bytes| u64::try_from(bytes).ok())
4818 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4819 let mut attempted = work
4820 .checked_add(WorkCounters {
4821 allocation_operations: u64::from(count != 0),
4822 ..WorkCounters::default()
4823 })
4824 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4825 attempted.peak_allocation_bytes = attempted.peak_allocation_bytes.max(
4826 temporary_bytes
4827 .checked_add(requested)
4828 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
4829 );
4830 attempted
4831 .verify(budget)
4832 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4833 entries
4834 .try_reserve_exact(count)
4835 .map_err(|_| LazyWorkspaceError::Work("directory page allocation failed".to_owned()))?;
4836 let allocated = entries
4837 .capacity()
4838 .checked_mul(std::mem::size_of::<LazyDirectoryEntry>())
4839 .and_then(|bytes| u64::try_from(bytes).ok())
4840 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4841 attempted.peak_allocation_bytes = attempted.peak_allocation_bytes.max(
4842 temporary_bytes
4843 .checked_add(allocated)
4844 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
4845 );
4846 attempted
4847 .verify(budget)
4848 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))?;
4849 *work = attempted;
4850 temporary_bytes
4851 .checked_add(allocated)
4852 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))
4853}
4854
4855fn exactify_path_vector_bytes(paths: &Vec<String>) -> Result<u64, LazyWorkspaceError> {
4856 u64::try_from(paths.capacity())
4857 .ok()
4858 .and_then(|count| count.checked_mul(u64::try_from(std::mem::size_of::<String>()).ok()?))
4859 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))
4860}
4861
4862fn retain_exactify_string(
4863 value: &String,
4864 retained_bytes: &mut u64,
4865 work: &mut WorkCounters,
4866 budget: WorkBudget,
4867) -> Result<(), LazyWorkspaceError> {
4868 let capacity = u64::try_from(value.capacity())
4869 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4870 let mut delta = WorkCounters {
4871 bytes_copied: u64::try_from(value.len())
4872 .map_err(|_| LazyWorkspaceError::Work("counter overflow".to_owned()))?,
4873 allocation_operations: u64::from(capacity != 0),
4874 ..WorkCounters::default()
4875 };
4876 *retained_bytes = retained_bytes
4877 .checked_add(capacity)
4878 .ok_or_else(|| LazyWorkspaceError::Work("counter overflow".to_owned()))?;
4879 delta.peak_allocation_bytes = *retained_bytes;
4880 *work = account_work(*work, delta, budget)?;
4881 Ok(())
4882}
4883
4884fn remaining_work(
4885 current: WorkCounters,
4886 budget: WorkBudget,
4887) -> Result<WorkBudget, LazyWorkspaceError> {
4888 current
4889 .remaining(budget)
4890 .map_err(|error| LazyWorkspaceError::Work(error.to_string()))
4891}
4892
4893fn exactify_error(stage: &str, path: &str, error: LazyWorkspaceError) -> LazyWorkspaceError {
4894 match error {
4895 LazyWorkspaceError::Cancelled | LazyWorkspaceError::Demand(DemandError::Cancelled) => {
4896 LazyWorkspaceError::Cancelled
4897 }
4898 error @ LazyWorkspaceError::Work(_) => error,
4899 error => {
4900 LazyWorkspaceError::Workspace(format!("exactify could not {stage} {path}: {error}"))
4901 }
4902 }
4903}
4904
4905fn exactify_batch_key(snapshot_id: LazySnapshotId, root: &str, index: u64) -> IdempotencyKey {
4906 let mut hasher = blake3::Hasher::new();
4907 hasher.update(b"acyclic-fs-lazy-exactify-batch-v1\0");
4908 hasher.update(&snapshot_id.into_bytes());
4909 hasher.update(root.as_bytes());
4910 hasher.update(&index.to_le_bytes());
4911 let mut bytes = [0; 16];
4912 bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
4913 IdempotencyKey::from_bytes(bytes)
4914}
4915
4916#[cfg(test)]
4917#[allow(clippy::expect_used, clippy::panic)]
4918mod tests {
4919 use super::*;
4920 use crate::demand::{
4921 DemandResult, SourceCursor, SourceDirectoryEntry, SourceDirectoryPage, SourceMetadata,
4922 SourceVersion,
4923 };
4924 use crate::performance::{OperationFailure, OperationReceipt, WorkCounters};
4925 use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
4926
4927 #[derive(Default)]
4928 struct SourceCounts {
4929 lookups: AtomicUsize,
4930 pages: AtomicUsize,
4931 ranges: AtomicUsize,
4932 }
4933
4934 struct CountingSource {
4935 identity: [u8; 16],
4936 epoch: AtomicU64,
4937 node: SourceNode,
4938 versions: Mutex<BTreeMap<u64, Bytes>>,
4939 directory_entries: usize,
4940 counts: SourceCounts,
4941 cancel_on_page: AtomicBool,
4942 cancel_on_range: AtomicBool,
4943 }
4944
4945 impl CountingSource {
4946 fn new(bytes: Bytes) -> Self {
4947 let logical_bytes = bytes.len() as u64;
4948 let versions = BTreeMap::from([(3, bytes)]);
4949 Self {
4950 identity: [7; 16],
4951 epoch: AtomicU64::new(3),
4952 node: SourceNode {
4953 kind: SourceNodeKind::RegularFile,
4954 file_identity: [8; 32],
4955 link_count: Some(1),
4956 device: None,
4957 logical_bytes: Some(logical_bytes),
4958 version: SourceVersion([9; 32]),
4959 metadata: SourceMetadata::default(),
4960 },
4961 versions: Mutex::new(versions),
4962 directory_entries: 1,
4963 counts: SourceCounts::default(),
4964 cancel_on_page: AtomicBool::new(false),
4965 cancel_on_range: AtomicBool::new(false),
4966 }
4967 }
4968
4969 fn receipt<T>(value: T) -> DemandResult<T> {
4970 Ok(OperationReceipt {
4971 value,
4972 work: WorkCounters::default(),
4973 })
4974 }
4975
4976 fn invalidate(&self) {
4977 let prior = self.epoch.fetch_add(1, Ordering::Relaxed);
4978 let mut versions = self
4979 .versions
4980 .lock()
4981 .unwrap_or_else(|poisoned| poisoned.into_inner());
4982 let bytes = versions.get(&prior).cloned().unwrap_or_default();
4983 versions.insert(prior + 1, bytes);
4984 }
4985
4986 fn replace(&self, bytes: Bytes) {
4987 let epoch = self.epoch.fetch_add(1, Ordering::Relaxed) + 1;
4988 self.versions
4989 .lock()
4990 .unwrap_or_else(|poisoned| poisoned.into_inner())
4991 .insert(epoch, bytes);
4992 }
4993
4994 fn versioned_node(&self, source: SourceReference) -> Option<SourceNode> {
4995 if source.identity != self.identity {
4996 return None;
4997 }
4998 let versions = self
4999 .versions
5000 .lock()
5001 .unwrap_or_else(|poisoned| poisoned.into_inner());
5002 let bytes = versions.get(&source.epoch)?;
5003 Some(SourceNode {
5004 logical_bytes: Some(bytes.len() as u64),
5005 version: SourceVersion(*blake3::hash(bytes).as_bytes()),
5006 ..self.node
5007 })
5008 }
5009
5010 fn with_directory_entries(mut self, entries: usize) -> Self {
5011 self.directory_entries = entries;
5012 self
5013 }
5014
5015 fn cancel_next_page(&self) {
5016 self.cancel_on_page.store(true, Ordering::Relaxed);
5017 }
5018
5019 fn cancel_next_range(&self) {
5020 self.cancel_on_range.store(true, Ordering::Relaxed);
5021 }
5022 }
5023
5024 #[async_trait]
5025 impl DemandSource for CountingSource {
5026 fn reference(&self) -> SourceReference {
5027 SourceReference {
5028 identity: self.identity,
5029 epoch: self.epoch.load(Ordering::Relaxed),
5030 }
5031 }
5032
5033 async fn lookup(
5034 &self,
5035 source: SourceReference,
5036 _path: &NamespacePath,
5037 _cancellation: &CancellationToken,
5038 ) -> DemandResult<Option<SourceNode>> {
5039 self.counts.lookups.fetch_add(1, Ordering::Relaxed);
5040 let Some(node) = self.versioned_node(source) else {
5041 return Err(OperationFailure::before_work(DemandError::StaleSource));
5042 };
5043 Self::receipt(Some(node))
5044 }
5045
5046 async fn list_page(
5047 &self,
5048 source: SourceReference,
5049 _directory: &NamespacePath,
5050 cursor: Option<SourceCursor>,
5051 _maximum_entries: u32,
5052 cancellation: &CancellationToken,
5053 ) -> DemandResult<SourceDirectoryPage> {
5054 self.counts.pages.fetch_add(1, Ordering::Relaxed);
5055 if self.cancel_on_page.swap(false, Ordering::Relaxed) {
5056 cancellation.cancel();
5057 return Err(OperationFailure::before_work(DemandError::Cancelled));
5058 }
5059 if self.versioned_node(source).is_none() {
5060 return Err(OperationFailure::before_work(DemandError::StaleSource));
5061 }
5062 if cursor.is_some() {
5063 return Err(OperationFailure::before_work(DemandError::StaleCursor));
5064 }
5065 let entries = if self.directory_entries == 1 {
5066 vec![SourceDirectoryEntry {
5067 name: LogicalName::new(NameEncoding::Utf8, b"file.txt".to_vec(), 255)
5068 .expect("name"),
5069 kind: SourceNodeKind::RegularFile,
5070 }]
5071 } else {
5072 (0..self.directory_entries)
5073 .map(|index| SourceDirectoryEntry {
5074 name: LogicalName::new(
5075 NameEncoding::Utf8,
5076 format!("file-{index:04}.txt").into_bytes(),
5077 255,
5078 )
5079 .expect("name"),
5080 kind: SourceNodeKind::RegularFile,
5081 })
5082 .collect()
5083 };
5084 Self::receipt(SourceDirectoryPage {
5085 entries,
5086 next: None,
5087 version: SourceVersion([4; 32]),
5088 })
5089 }
5090
5091 async fn read_range(
5092 &self,
5093 source: SourceReference,
5094 _path: &NamespacePath,
5095 expected: SourceVersion,
5096 offset: u64,
5097 length: u64,
5098 cancellation: &CancellationToken,
5099 ) -> DemandResult<Bytes> {
5100 self.counts.ranges.fetch_add(1, Ordering::Relaxed);
5101 if self.cancel_on_range.swap(false, Ordering::Relaxed) {
5102 cancellation.cancel();
5103 }
5104 let Some(node) = self.versioned_node(source) else {
5105 return Err(OperationFailure::before_work(DemandError::StaleSource));
5106 };
5107 if expected != node.version {
5108 return Err(OperationFailure::before_work(DemandError::StaleVersion));
5109 }
5110 let versions = self
5111 .versions
5112 .lock()
5113 .unwrap_or_else(|poisoned| poisoned.into_inner());
5114 let bytes = versions
5115 .get(&source.epoch)
5116 .ok_or_else(|| OperationFailure::before_work(DemandError::StaleSource))?;
5117 let start = usize::try_from(offset)
5118 .unwrap_or(usize::MAX)
5119 .min(bytes.len());
5120 let end = start
5121 .saturating_add(usize::try_from(length).unwrap_or(usize::MAX))
5122 .min(bytes.len());
5123 Self::receipt(bytes.slice(start..end))
5124 }
5125
5126 async fn read_link(
5127 &self,
5128 _source: SourceReference,
5129 _path: &NamespacePath,
5130 _expected: SourceVersion,
5131 _cancellation: &CancellationToken,
5132 ) -> DemandResult<Bytes> {
5133 Err(OperationFailure::before_work(DemandError::InvalidRequest))
5134 }
5135 }
5136
5137 #[tokio::test]
5138 async fn exactify_threads_cancellation_into_nested_source_work() {
5139 let fs = Fs::memory();
5140 let source = Arc::new(CountingSource::new(Bytes::from_static(b"content")));
5141 let root = LazyWorkspace::attach(
5142 &fs,
5143 "cancelled-exactify",
5144 Arc::clone(&source),
5145 MemoryLazyWorkspaceStore::default(),
5146 )
5147 .await
5148 .expect("attach");
5149 let cancellation = CancellationToken::new();
5150 source.cancel_next_page();
5151
5152 assert!(matches!(
5153 root.exactify(WorkBudget::UNBOUNDED, &cancellation).await,
5154 Err(LazyWorkspaceError::Cancelled)
5155 ));
5156 assert!(cancellation.is_cancelled());
5157 assert_eq!(source.counts.pages.load(Ordering::Relaxed), 1);
5158 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 0);
5159 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 0);
5160 }
5161
5162 #[tokio::test]
5163 async fn exactify_budget_exhaustion_cannot_publish_the_current_path() {
5164 let fs = Fs::memory();
5165 let source = Arc::new(CountingSource::new(Bytes::from_static(b"content")));
5166 let root = LazyWorkspace::attach(
5167 &fs,
5168 "budgeted-exactify",
5169 source,
5170 MemoryLazyWorkspaceStore::default(),
5171 )
5172 .await
5173 .expect("attach");
5174 let before = root
5175 .workspace()
5176 .head()
5177 .await
5178 .expect("head before exactify")
5179 .id();
5180 let mut budget = WorkBudget::UNBOUNDED;
5181 budget.authority_records_appended = 0;
5182
5183 assert!(matches!(
5184 root.exactify(budget, &CancellationToken::new()).await,
5185 Err(LazyWorkspaceError::Workspace(_))
5186 ));
5187 assert_eq!(
5188 root.workspace()
5189 .head()
5190 .await
5191 .expect("head after rejection")
5192 .id(),
5193 before
5194 );
5195 assert!(matches!(
5196 root.workspace().stat("/file.txt").await,
5197 Err(WorkspaceError::NotFound)
5198 ));
5199 }
5200
5201 #[tokio::test]
5202 async fn exactify_snapshot_rejects_before_destination_publication() {
5203 let fs = Fs::memory();
5204 let root = LazyWorkspace::attach(
5205 &fs,
5206 "snapshot-source",
5207 Arc::new(CountingSource::new(Bytes::from_static(b"content"))),
5208 MemoryLazyWorkspaceStore::default(),
5209 )
5210 .await
5211 .expect("attach");
5212 let snapshot = root.snapshot().await.expect("snapshot");
5213
5214 let cancelled = CancellationToken::new();
5215 cancelled.cancel();
5216 assert!(matches!(
5217 root.exactify_snapshot(
5218 snapshot,
5219 "cancelled-destination",
5220 IdempotencyKey::from_bytes([0x91; 16]),
5221 WorkBudget::UNBOUNDED,
5222 &cancelled,
5223 )
5224 .await,
5225 Err(LazyWorkspaceError::Cancelled)
5226 ));
5227 assert!(fs.open_workspace("cancelled-destination").await.is_err());
5228
5229 assert!(matches!(
5230 root.exactify_snapshot(
5231 snapshot,
5232 "budget-destination",
5233 IdempotencyKey::from_bytes([0x92; 16]),
5234 WorkBudget::default(),
5235 &CancellationToken::new(),
5236 )
5237 .await,
5238 Err(LazyWorkspaceError::Work(_))
5239 ));
5240 assert!(fs.open_workspace("budget-destination").await.is_err());
5241 }
5242
5243 #[tokio::test]
5244 async fn full_directory_page_peak_is_admitted_before_projection() {
5245 let fs = Fs::memory();
5246 let root = LazyWorkspace::attach(
5247 &fs,
5248 "page-memory",
5249 Arc::new(
5250 CountingSource::new(Bytes::from_static(b"content")).with_directory_entries(1_024),
5251 ),
5252 MemoryLazyWorkspaceStore::default(),
5253 )
5254 .await
5255 .expect("attach");
5256 let receipt = root
5257 .list_directory_measured(
5258 "/",
5259 None,
5260 1_024,
5261 WorkBudget::UNBOUNDED,
5262 &CancellationToken::new(),
5263 None,
5264 None,
5265 )
5266 .await
5267 .expect("measured page");
5268 assert_eq!(receipt.value.entries.len(), 1_024);
5269 assert!(receipt.work.allocation_operations >= 1);
5270
5271 let mut insufficient = WorkBudget::UNBOUNDED;
5272 insufficient.peak_allocation_bytes = receipt.work.peak_allocation_bytes - 1;
5273 assert!(matches!(
5274 root.list_directory_measured(
5275 "/",
5276 None,
5277 1_024,
5278 insufficient,
5279 &CancellationToken::new(),
5280 None,
5281 None,
5282 )
5283 .await,
5284 Err(LazyWorkspaceError::Work(_))
5285 ));
5286 }
5287
5288 #[tokio::test]
5289 async fn attach_open_and_fork_demand_no_source_work() {
5290 let fs = Fs::memory();
5291 let source = Arc::new(CountingSource::new(Bytes::from_static(b"abcdef")));
5292 let store = MemoryLazyWorkspaceStore::default();
5293 let root = LazyWorkspace::attach(&fs, "root", Arc::clone(&source), store.clone())
5294 .await
5295 .expect("attach");
5296 let reopened = LazyWorkspace::open(
5297 fs.open_workspace("root").await.expect("workspace"),
5298 Arc::clone(&source),
5299 store.clone(),
5300 )
5301 .await
5302 .expect("open");
5303 let child = reopened
5304 .fork("child", IdempotencyKey::from_bytes([5; 16]))
5305 .await
5306 .expect("fork");
5307 let first_snapshot = child.snapshot().await.expect("snapshot");
5308 let second_snapshot = child.snapshot().await.expect("repeat snapshot");
5309 let resumed_child = root
5310 .open_related(fs.open_workspace("child").await.expect("child workspace"))
5311 .await
5312 .expect("resume related child");
5313 let sibling = root
5314 .fork("sibling", IdempotencyKey::from_bytes([6; 16]))
5315 .await
5316 .expect("sibling fork");
5317 let resumed_sibling = resumed_child
5318 .open_related(
5319 fs.open_workspace("sibling")
5320 .await
5321 .expect("sibling workspace"),
5322 )
5323 .await
5324 .expect("constant-work branch switch");
5325 assert_eq!(
5326 sibling.snapshot().await.expect("sibling snapshot"),
5327 resumed_sibling
5328 .snapshot()
5329 .await
5330 .expect("resumed sibling snapshot")
5331 );
5332
5333 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 0);
5334 assert_eq!(source.counts.pages.load(Ordering::Relaxed), 0);
5335 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 0);
5336 assert_eq!(
5337 child
5338 .state()
5339 .await
5340 .expect("child state")
5341 .parent_workspace_id,
5342 Some(root.workspace().id())
5343 );
5344 assert_eq!(first_snapshot, second_snapshot);
5345 }
5346
5347 #[cfg(feature = "native-mount")]
5348 #[tokio::test]
5349 async fn mounted_promotion_cancels_midstream_without_installing_a_partial_candidate() {
5350 let fs = Fs::memory();
5351 let source = Arc::new(CountingSource::new(Bytes::from(vec![
5352 0x5a;
5353 2 * 1024 * 1024
5354 ])));
5355 let root = LazyWorkspace::attach(
5356 &fs,
5357 "cancelled-mount-promotion",
5358 Arc::clone(&source),
5359 MemoryLazyWorkspaceStore::default(),
5360 )
5361 .await
5362 .expect("attach");
5363 let head = root.workspace().head().await.expect("head").id();
5364 let mut checkout = root
5365 .workspace()
5366 .checkout(
5367 crate::model::GenerationSelector::Head,
5368 crate::model::CheckoutMode::tracking_transaction(),
5369 )
5370 .await
5371 .expect("checkout");
5372 let LazyLookup::Source(node) = root.lookup("/file.txt").await.expect("source") else {
5373 panic!("expected unresolved source file");
5374 };
5375 let cancellation = CancellationToken::new();
5376 source.cancel_next_range();
5377 assert!(matches!(
5378 root.stage_exact_into_checkout(
5379 &mut checkout,
5380 "/file.txt",
5381 root.source_file_id(&node),
5382 u64::MAX,
5383 &cancellation,
5384 )
5385 .await,
5386 Err(LazyWorkspaceError::Cancelled)
5387 ));
5388 assert!(cancellation.is_cancelled());
5389 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 1);
5390 assert_eq!(root.workspace().head().await.expect("head").id(), head);
5391 assert!(
5392 checkout
5393 .lookup_no_follow(
5394 &root.namespace_path("/file.txt").expect("path"),
5395 WorkBudget::UNBOUNDED,
5396 &CancellationToken::new(),
5397 )
5398 .await
5399 .expect("candidate lookup")
5400 .value
5401 .record
5402 .is_none()
5403 );
5404 }
5405
5406 #[cfg(feature = "native-mount")]
5407 #[tokio::test]
5408 async fn mounted_promotion_stays_in_the_dirty_candidate_until_one_publication() {
5409 let fs = Fs::memory();
5410 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
5411 let root = LazyWorkspace::attach(
5412 &fs,
5413 "staged-mount-promotion",
5414 source,
5415 MemoryLazyWorkspaceStore::default(),
5416 )
5417 .await
5418 .expect("attach");
5419 let original_head = root.workspace().head().await.expect("head").id();
5420 let mut checkout = root
5421 .workspace()
5422 .checkout(
5423 crate::model::GenerationSelector::Head,
5424 crate::model::CheckoutMode::tracking_transaction(),
5425 )
5426 .await
5427 .expect("mount checkout");
5428 let other = root.namespace_path("/other.txt").expect("other path");
5429 checkout
5430 .apply_authored_transaction(
5431 vec![AuthoredMutation::CreateFile {
5432 path: other.clone(),
5433 bytes: Bytes::from_static(b"dirty"),
5434 metadata: FileMetadata::default(),
5435 }],
5436 WorkBudget::UNBOUNDED,
5437 &CancellationToken::new(),
5438 )
5439 .await
5440 .expect("dirty mounted write");
5441 let LazyLookup::Source(node) = root.lookup("/file.txt").await.expect("source") else {
5442 panic!("expected unresolved source file");
5443 };
5444 let cancelled = CancellationToken::new();
5445 cancelled.cancel();
5446 assert!(matches!(
5447 root.stage_exact_into_checkout(
5448 &mut checkout,
5449 "/file.txt",
5450 root.source_file_id(&node),
5451 u64::MAX,
5452 &cancelled,
5453 )
5454 .await,
5455 Err(LazyWorkspaceError::Cancelled)
5456 ));
5457 assert!(
5458 root.stage_exact_into_checkout(
5459 &mut checkout,
5460 "/file.txt",
5461 root.source_file_id(&node),
5462 u64::MAX,
5463 &CancellationToken::new(),
5464 )
5465 .await
5466 .expect("stage source in dirty mount")
5467 );
5468 assert!(
5469 !root
5470 .stage_exact_into_checkout(
5471 &mut checkout,
5472 "/file.txt",
5473 root.source_file_id(&node),
5474 u64::MAX,
5475 &CancellationToken::new(),
5476 )
5477 .await
5478 .expect("already staged source identity")
5479 );
5480 assert_eq!(
5481 root.workspace()
5482 .head()
5483 .await
5484 .expect("unpublished head")
5485 .id(),
5486 original_head
5487 );
5488 let file = root.namespace_path("/file.txt").expect("file path");
5489 for path in [&other, &file] {
5490 assert!(
5491 checkout
5492 .lookup_no_follow(path, WorkBudget::UNBOUNDED, &CancellationToken::new())
5493 .await
5494 .expect("candidate lookup")
5495 .value
5496 .record
5497 .is_some()
5498 );
5499 }
5500 checkout
5501 .commit(
5502 crate::OperationId::new(),
5503 WorkBudget::UNBOUNDED,
5504 &CancellationToken::new(),
5505 )
5506 .await
5507 .expect("publish both changes once");
5508 assert_ne!(
5509 root.workspace().head().await.expect("published head").id(),
5510 original_head
5511 );
5512 }
5513
5514 #[cfg(feature = "native-mount")]
5515 #[tokio::test]
5516 async fn mounted_alias_promotions_preserve_source_hard_links_before_publication() {
5517 let fs = Fs::memory();
5518 let mut source = CountingSource::new(Bytes::from_static(b"shared"));
5519 source.node.link_count = Some(2);
5520 let root = LazyWorkspace::attach(
5521 &fs,
5522 "staged-hard-link-promotions",
5523 Arc::new(source),
5524 MemoryLazyWorkspaceStore::default(),
5525 )
5526 .await
5527 .expect("attach");
5528 let mut checkout = root
5529 .workspace()
5530 .checkout(
5531 crate::model::GenerationSelector::Head,
5532 crate::model::CheckoutMode::tracking_transaction(),
5533 )
5534 .await
5535 .expect("mount checkout");
5536 for path in ["/a", "/b"] {
5537 let LazyLookup::Source(node) = root.lookup(path).await.expect("source alias") else {
5538 panic!("expected unresolved source alias");
5539 };
5540 assert!(
5541 root.stage_exact_into_checkout(
5542 &mut checkout,
5543 path,
5544 root.source_file_id(&node),
5545 u64::MAX,
5546 &CancellationToken::new(),
5547 )
5548 .await
5549 .expect("stage source alias")
5550 );
5551 }
5552 let first = checkout
5553 .lookup_no_follow(
5554 &root.namespace_path("/a").expect("first path"),
5555 WorkBudget::UNBOUNDED,
5556 &CancellationToken::new(),
5557 )
5558 .await
5559 .expect("first alias")
5560 .value
5561 .record
5562 .expect("first record");
5563 let second = checkout
5564 .lookup_no_follow(
5565 &root.namespace_path("/b").expect("second path"),
5566 WorkBudget::UNBOUNDED,
5567 &CancellationToken::new(),
5568 )
5569 .await
5570 .expect("second alias")
5571 .value
5572 .record
5573 .expect("second record");
5574 assert_eq!(first.file_id, second.file_id);
5575 assert_eq!(first.link_count, 2);
5576 assert_eq!(second.link_count, 2);
5577 }
5578
5579 #[cfg(feature = "native-mount")]
5580 #[tokio::test]
5581 async fn mounted_directory_pages_include_unpublished_checkout_bindings() {
5582 let fs = Fs::memory();
5583 let root = LazyWorkspace::attach(
5584 &fs,
5585 "mounted-directory-candidate",
5586 Arc::new(CountingSource::new(Bytes::from_static(b"source"))),
5587 MemoryLazyWorkspaceStore::default(),
5588 )
5589 .await
5590 .expect("attach");
5591 let mut checkout = root
5592 .workspace()
5593 .checkout(
5594 crate::model::GenerationSelector::Head,
5595 crate::model::CheckoutMode::tracking_transaction(),
5596 )
5597 .await
5598 .expect("mount checkout");
5599 checkout
5600 .apply_authored_transaction(
5601 vec![AuthoredMutation::CreateFile {
5602 path: root.namespace_path("/new.txt").expect("new path"),
5603 bytes: Bytes::from_static(b"new"),
5604 metadata: FileMetadata::default(),
5605 }],
5606 WorkBudget::UNBOUNDED,
5607 &CancellationToken::new(),
5608 )
5609 .await
5610 .expect("unpublished file");
5611 let source_page = root
5612 .list_directory_in_checkout(&mut checkout, "/", None, 16, None)
5613 .await
5614 .expect("source page");
5615 assert_eq!(source_page.entries.len(), 1);
5616 assert!(!source_page.entries[0].authored);
5617 let authored_page = root
5618 .list_directory_in_checkout(&mut checkout, "/", source_page.next, 16, None)
5619 .await
5620 .expect("authored page");
5621 assert_eq!(authored_page.entries.len(), 1);
5622 assert!(authored_page.entries[0].authored);
5623 assert_eq!(authored_page.entries[0].name.as_bytes(), b"new.txt");
5624 assert!(authored_page.next.is_none());
5625 }
5626
5627 #[cfg(all(
5628 feature = "native-mount",
5629 feature = "native-watch",
5630 not(target_arch = "wasm32")
5631 ))]
5632 #[tokio::test]
5633 async fn mounted_replacement_directory_does_not_expose_obsolete_source_children() {
5634 use crate::demand::native::NativeDemandSource;
5635 use crate::model::{FilesystemProfile, VolumeLimits};
5636
5637 let directory = tempfile::tempdir().expect("temporary source");
5638 std::fs::create_dir(directory.path().join("d")).expect("source directory");
5639 std::fs::write(directory.path().join("d").join("old.txt"), b"old").expect("source child");
5640 let source = Arc::new(
5641 NativeDemandSource::open(
5642 directory.path(),
5643 FilesystemProfile::Portable,
5644 VolumeLimits::default(),
5645 )
5646 .await
5647 .expect("source"),
5648 );
5649 let fs = Fs::memory();
5650 let root = LazyWorkspace::attach(
5651 &fs,
5652 "mounted-directory-replacement",
5653 source,
5654 MemoryLazyWorkspaceStore::default(),
5655 )
5656 .await
5657 .expect("attach");
5658 let mut checkout = root
5659 .workspace()
5660 .checkout(
5661 crate::model::GenerationSelector::Head,
5662 crate::model::CheckoutMode::tracking_transaction(),
5663 )
5664 .await
5665 .expect("mount checkout");
5666 assert_eq!(
5667 root.list_directory_in_checkout(&mut checkout, "/d", None, 16, None)
5668 .await
5669 .expect("source directory")
5670 .entries
5671 .len(),
5672 1
5673 );
5674 checkout
5675 .apply_authored_transaction(
5676 vec![AuthoredMutation::CreateDirectory {
5677 path: root.namespace_path("/d").expect("replacement path"),
5678 metadata: FileMetadata::default(),
5679 }],
5680 WorkBudget::UNBOUNDED,
5681 &CancellationToken::new(),
5682 )
5683 .await
5684 .expect("replace with empty authored directory");
5685 let page = root
5686 .list_directory_in_checkout(&mut checkout, "/d", None, 16, None)
5687 .await
5688 .expect("replacement directory");
5689 assert!(page.entries.is_empty());
5690 assert!(page.next.is_none());
5691 }
5692
5693 #[tokio::test]
5694 async fn source_only_remove_does_not_advance_the_authored_head() {
5695 let fs = Fs::memory();
5696 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
5697 let root = LazyWorkspace::attach(
5698 &fs,
5699 "source-only-remove-effect",
5700 source,
5701 MemoryLazyWorkspaceStore::default(),
5702 )
5703 .await
5704 .expect("attach");
5705 let original_head = root.workspace().head().await.expect("head").id();
5706 let LazyLookup::Source(node) = root.lookup("/file.txt").await.expect("source") else {
5707 panic!("expected source file");
5708 };
5709 root.remove_if("/file.txt", Some(root.source_file_id(&node)))
5710 .await
5711 .expect("source-only tombstone");
5712 assert_eq!(
5713 root.workspace().head().await.expect("unchanged head").id(),
5714 original_head
5715 );
5716 assert!(matches!(
5717 root.lookup("/file.txt").await,
5718 Err(LazyWorkspaceError::NotFound)
5719 ));
5720 }
5721
5722 #[cfg(feature = "native-mount")]
5723 #[tokio::test]
5724 async fn mounted_whiteout_follows_the_checkout_publication_decision() {
5725 let fs = Fs::memory();
5726 let root = LazyWorkspace::attach(
5727 &fs,
5728 "mounted-whiteout-publication",
5729 Arc::new(CountingSource::new(Bytes::from_static(b"source"))),
5730 MemoryLazyWorkspaceStore::default(),
5731 )
5732 .await
5733 .expect("attach");
5734 let paths = vec!["/file.txt".to_owned()];
5735 let aborted = IdempotencyKey::new();
5736 root.prepare_mount_removals(&paths, &[], aborted)
5737 .await
5738 .expect("prepare uncommitted whiteout");
5739 assert!(!root.finish_mount_removals().await.expect("rollback"));
5740 assert!(matches!(
5741 root.lookup("/file.txt").await.expect("source remains"),
5742 LazyLookup::Source(_)
5743 ));
5744
5745 let committed = IdempotencyKey::new();
5746 root.prepare_mount_removals(&paths, &[], committed)
5747 .await
5748 .expect("prepare committed whiteout");
5749 let mut checkout = root
5750 .workspace()
5751 .checkout(
5752 crate::model::GenerationSelector::Head,
5753 crate::model::CheckoutMode::tracking_transaction(),
5754 )
5755 .await
5756 .expect("checkout");
5757 assert!(matches!(
5758 checkout
5759 .commit_with_permit_even_if_clean(
5760 committed.operation_id(),
5761 crate::PublicationPermit::Unrestricted,
5762 WorkBudget::UNBOUNDED,
5763 &CancellationToken::new(),
5764 )
5765 .await
5766 .expect("publish unchanged authored root")
5767 .value,
5768 crate::CheckoutCommitOutcome::Committed { .. }
5769 ));
5770 assert!(root.finish_mount_removals().await.expect("finalize"));
5771 assert!(matches!(
5772 root.lookup("/file.txt").await,
5773 Err(LazyWorkspaceError::NotFound)
5774 ));
5775 }
5776
5777 #[tokio::test]
5778 async fn exact_observation_is_shared_and_content_is_range_demanded() {
5779 let fs = Fs::memory();
5780 let source = Arc::new(CountingSource::new(Bytes::from_static(b"abcdef")));
5781 let store = MemoryLazyWorkspaceStore::default();
5782 let root = LazyWorkspace::attach(&fs, "root", Arc::clone(&source), store)
5783 .await
5784 .expect("attach");
5785
5786 assert_eq!(
5787 root.stat("/deep/file.txt")
5788 .await
5789 .expect("stat")
5790 .logical_bytes,
5791 Some(6)
5792 );
5793 assert_eq!(
5794 root.stat("/deep/file.txt")
5795 .await
5796 .expect("cached stat")
5797 .logical_bytes,
5798 Some(6)
5799 );
5800 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 1);
5801 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 0);
5802 assert_eq!(
5803 root.read_range("/deep/file.txt", 2, 3)
5804 .await
5805 .expect("range"),
5806 Bytes::from_static(b"cde")
5807 );
5808 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 1);
5809 assert_eq!(source.counts.pages.load(Ordering::Relaxed), 0);
5810 }
5811
5812 #[tokio::test]
5813 async fn inspection_is_transient_and_resolved_reads_pin_the_source_version() {
5814 let fs = Fs::memory();
5815 let source = Arc::new(CountingSource::new(Bytes::from_static(b"before")));
5816 let root = LazyWorkspace::attach(
5817 &fs,
5818 "transient-inspection",
5819 Arc::clone(&source),
5820 MemoryLazyWorkspaceStore::default(),
5821 )
5822 .await
5823 .expect("attach");
5824 let before = root.state().await.expect("state before inspection");
5825
5826 let (lookup, source_reference) = root
5827 .inspect_resolved("/file.txt")
5828 .await
5829 .expect("inspect source file");
5830 let LazyLookup::Source(node) = lookup else {
5831 panic!("inspection must resolve the source file");
5832 };
5833 assert_eq!(root.state().await.expect("state after inspection"), before);
5834
5835 source.replace(Bytes::from_static(b"after"));
5836 assert_eq!(
5837 root.read_source_range(
5838 "/file.txt",
5839 source_reference.expect("source reference"),
5840 node,
5841 0,
5842 6,
5843 )
5844 .await
5845 .expect("read pinned version"),
5846 Bytes::from_static(b"before")
5847 );
5848
5849 root.rebind_source().await.expect("rebind source");
5850 let LazyLookup::Source(rebound) = root.inspect("/file.txt").await.expect("reinspect")
5851 else {
5852 panic!("rebound inspection must resolve the source file");
5853 };
5854 assert_ne!(rebound.version, node.version);
5855 }
5856
5857 #[tokio::test]
5858 async fn independent_alias_promotions_preserve_source_hard_links() {
5859 let fs = Fs::memory();
5860 let mut source = CountingSource::new(Bytes::from_static(b"shared"));
5861 source.node.link_count = Some(2);
5862 source.node.metadata.posix_mode = Some(0o100640);
5863 source.node.metadata.modified_ns = Some(123_456);
5864 let source = Arc::new(source);
5865 let root = LazyWorkspace::attach(
5866 &fs,
5867 "hard-link-promotions",
5868 Arc::clone(&source),
5869 MemoryLazyWorkspaceStore::default(),
5870 )
5871 .await
5872 .expect("attach");
5873
5874 root.promote("/a", 64, IdempotencyKey::from_bytes([0x71; 16]))
5875 .await
5876 .expect("promote first alias");
5877 root.promote("/b", 64, IdempotencyKey::from_bytes([0x72; 16]))
5878 .await
5879 .expect("promote second alias");
5880 let first = root.workspace().stat("/a").await.expect("first stat");
5881 let second = root.workspace().stat("/b").await.expect("second stat");
5882 assert_eq!(first.file_id, second.file_id);
5883 assert_eq!(first.file_id, root.source_file_id(&source.node));
5884 assert_eq!(first.link_count, 2);
5885 assert_eq!(second.link_count, 2);
5886 assert_eq!(first.metadata.posix_mode, Some(0o100640));
5887 assert_eq!(second.metadata.modified_ns, Some(123_456));
5888
5889 root.write("/a", Bytes::from_static(b"changed"))
5890 .await
5891 .expect("mutate alias");
5892 assert_eq!(
5893 root.workspace()
5894 .read("/b", 64)
5895 .await
5896 .expect("read other alias")
5897 .as_ref(),
5898 b"changed"
5899 );
5900 }
5901
5902 #[tokio::test]
5903 async fn unresolved_alias_with_unknown_link_count_reads_latest_authored_identity() {
5904 let fs = Fs::memory();
5905 let mut source = CountingSource::new(Bytes::from_static(b"source"));
5906 source.node.link_count = None;
5907 let source = Arc::new(source);
5908 let root = LazyWorkspace::attach(
5909 &fs,
5910 "hard-link-late-alias",
5911 Arc::clone(&source),
5912 MemoryLazyWorkspaceStore::default(),
5913 )
5914 .await
5915 .expect("attach");
5916
5917 root.promote("/a", 64, IdempotencyKey::from_bytes([0x73; 16]))
5918 .await
5919 .expect("promote first alias");
5920 root.write("/a", Bytes::from_static(b"changed"))
5921 .await
5922 .expect("mutate first alias");
5923 let source_reads = source.counts.ranges.load(Ordering::Relaxed);
5924 assert_eq!(
5925 root.read("/b", 64).await.expect("read alias").as_ref(),
5926 b"changed"
5927 );
5928 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), source_reads);
5929
5930 root.promote("/b", 64, IdempotencyKey::from_bytes([0x74; 16]))
5931 .await
5932 .expect("promote late alias");
5933 let first = root.workspace().stat("/a").await.expect("first stat");
5934 let second = root.workspace().stat("/b").await.expect("second stat");
5935 assert_eq!(first.file_id, second.file_id);
5936 assert_eq!(first.file_id, root.source_file_id(&source.node));
5937 assert_eq!(
5938 root.read("/b", 64)
5939 .await
5940 .expect("read promoted alias")
5941 .as_ref(),
5942 b"changed"
5943 );
5944 }
5945
5946 #[tokio::test]
5947 async fn identity_shadow_survives_last_promoted_alias_removal() {
5948 let fs = Fs::memory();
5949 let mut source = CountingSource::new(Bytes::from_static(b"source"));
5950 source.node.link_count = Some(2);
5951 let source = Arc::new(source);
5952 let store = MemoryLazyWorkspaceStore::default();
5953 let root =
5954 LazyWorkspace::attach(&fs, "hard-link-shadow", Arc::clone(&source), store.clone())
5955 .await
5956 .expect("attach");
5957
5958 root.write("/a", Bytes::from_static(b"changed"))
5959 .await
5960 .expect("direct identity-preserving write");
5961 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 0);
5962 root.remove("/a").await.expect("remove promoted alias");
5963 assert_eq!(
5964 root.read("/b", 64)
5965 .await
5966 .expect("read shadow alias")
5967 .as_ref(),
5968 b"changed"
5969 );
5970
5971 let reopened = LazyWorkspace::open(
5972 fs.open_workspace("hard-link-shadow")
5973 .await
5974 .expect("workspace"),
5975 source,
5976 store,
5977 )
5978 .await
5979 .expect("reopen");
5980 assert_eq!(
5981 reopened
5982 .read("/b", 64)
5983 .await
5984 .expect("reopened shadow")
5985 .as_ref(),
5986 b"changed"
5987 );
5988 }
5989
5990 #[tokio::test]
5991 async fn remove_if_rejects_a_stale_source_identity() {
5992 let fs = Fs::memory();
5993 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
5994 let root = LazyWorkspace::attach(
5995 &fs,
5996 "stale-remove",
5997 Arc::clone(&source),
5998 MemoryLazyWorkspaceStore::default(),
5999 )
6000 .await
6001 .expect("attach");
6002 let stale = FileId::from_bytes([0xff; 16]);
6003 assert!(matches!(
6004 root.remove_if("/file.txt", Some(stale)).await,
6005 Err(LazyWorkspaceError::StaleIdentity)
6006 ));
6007 assert!(root.stat("/file.txt").await.is_ok());
6008 }
6009
6010 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
6011 #[tokio::test]
6012 async fn metadata_inspection_does_not_stale_a_native_directory_cursor() {
6013 use crate::demand::native::NativeDemandSource;
6014 use crate::model::{FilesystemProfile, VolumeLimits};
6015
6016 let directory = tempfile::tempdir().expect("temporary source");
6017 std::fs::write(directory.path().join("a.txt"), b"a").expect("first file");
6018 std::fs::write(directory.path().join("b.txt"), b"b").expect("second file");
6019 let source = Arc::new(
6020 NativeDemandSource::open(
6021 directory.path(),
6022 FilesystemProfile::Portable,
6023 VolumeLimits::default(),
6024 )
6025 .await
6026 .expect("source"),
6027 );
6028 let fs = Fs::memory();
6029 let root = LazyWorkspace::attach(
6030 &fs,
6031 "paged-source",
6032 source,
6033 MemoryLazyWorkspaceStore::default(),
6034 )
6035 .await
6036 .expect("attach");
6037 let first = root.list_directory("/", None, 1).await.expect("first page");
6038 let name = std::str::from_utf8(first.entries[0].name.as_bytes()).expect("portable name");
6039 root.inspect(&format!("/{name}"))
6040 .await
6041 .expect("entry metadata");
6042 let second = root
6043 .list_directory("/", first.next, 1)
6044 .await
6045 .expect("second page remains valid");
6046 assert_eq!(second.entries.len(), 1);
6047 }
6048
6049 #[tokio::test]
6050 async fn unresolved_aliases_have_one_stable_identity_and_dense_seek_needs_no_content() {
6051 let fs = Fs::memory();
6052 let source = Arc::new(CountingSource::new(Bytes::from_static(b"abcdef")));
6053 let root = LazyWorkspace::attach(
6054 &fs,
6055 "root",
6056 Arc::clone(&source),
6057 MemoryLazyWorkspaceStore::default(),
6058 )
6059 .await
6060 .expect("attach");
6061 let first = match root.lookup("/first").await.expect("first") {
6062 LazyLookup::Source(node) => node,
6063 LazyLookup::Authored { .. } | LazyLookup::Shadow { .. } => {
6064 panic!("source lookup expected")
6065 }
6066 };
6067 let second = match root.lookup("/second").await.expect("second") {
6068 LazyLookup::Source(node) => node,
6069 LazyLookup::Authored { .. } | LazyLookup::Shadow { .. } => {
6070 panic!("source lookup expected")
6071 }
6072 };
6073 assert_eq!(root.source_file_id(&first), root.source_file_id(&second));
6074 assert_eq!(
6075 root.seek("/first", 2, LazySeekTarget::Data)
6076 .await
6077 .expect("seek data"),
6078 Some(2)
6079 );
6080 assert_eq!(
6081 root.seek("/first", 2, LazySeekTarget::Hole)
6082 .await
6083 .expect("seek hole"),
6084 Some(6)
6085 );
6086 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 0);
6087 }
6088
6089 #[tokio::test]
6090 async fn promotion_demands_only_the_selected_file_and_is_idempotent() {
6091 let fs = Fs::memory();
6092 let bytes = Bytes::from(vec![b'x'; 1024 * 1024 + 17]);
6093 let source = Arc::new(CountingSource::new(bytes.clone()));
6094 let root = LazyWorkspace::attach(
6095 &fs,
6096 "root",
6097 Arc::clone(&source),
6098 MemoryLazyWorkspaceStore::default(),
6099 )
6100 .await
6101 .expect("attach");
6102 let key = IdempotencyKey::from_bytes([21; 16]);
6103
6104 let first = root
6105 .promote("/deep/file.txt", bytes.len() as u64, key)
6106 .await
6107 .expect("promote");
6108 assert!(matches!(first, TransactionCommit::Committed(_)));
6109 assert_eq!(
6110 root.workspace()
6111 .read("/deep/file.txt", bytes.len() as u64)
6112 .await
6113 .expect("authored content"),
6114 bytes
6115 );
6116 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 1);
6117 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 2);
6118 assert_eq!(source.counts.pages.load(Ordering::Relaxed), 0);
6119
6120 let second = root
6121 .promote("/deep/file.txt", 0, key)
6122 .await
6123 .expect("idempotent promotion");
6124 assert!(matches!(second, TransactionCommit::AlreadyCommitted(_)));
6125 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 1);
6126 assert_eq!(source.counts.ranges.load(Ordering::Relaxed), 2);
6127 }
6128
6129 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
6130 #[tokio::test]
6131 async fn promoted_source_directory_keeps_its_identity_and_lazy_children() {
6132 use crate::demand::native::NativeDemandSource;
6133 use crate::model::{FilesystemProfile, VolumeLimits};
6134
6135 let directory = tempfile::tempdir().expect("temporary source");
6136 std::fs::create_dir(directory.path().join("repo")).expect("source directory");
6137 std::fs::write(
6138 directory.path().join("repo/HEAD"),
6139 b"ref: refs/heads/main\n",
6140 )
6141 .expect("source child");
6142 let source = Arc::new(
6143 NativeDemandSource::open(
6144 directory.path(),
6145 FilesystemProfile::Portable,
6146 VolumeLimits::default(),
6147 )
6148 .await
6149 .expect("source"),
6150 );
6151 let root = LazyWorkspace::attach(
6152 &Fs::memory(),
6153 "directory-identity",
6154 source,
6155 MemoryLazyWorkspaceStore::default(),
6156 )
6157 .await
6158 .expect("attach");
6159 let source_node = match root.lookup("/repo").await.expect("source directory") {
6160 LazyLookup::Source(node) => node,
6161 LazyLookup::Authored { .. } | LazyLookup::Shadow { .. } => {
6162 panic!("expected source directory")
6163 }
6164 };
6165 let source_id = root.source_file_id(&source_node);
6166
6167 root.promote("/repo", 0, IdempotencyKey::from_bytes([0x52; 16]))
6168 .await
6169 .expect("promote directory");
6170
6171 let promoted = root.lookup("/repo").await.expect("promoted directory");
6172 assert_eq!(
6173 root.stable_file_id_for_lookup("/repo", &promoted)
6174 .await
6175 .expect("stable projection identity"),
6176 source_id
6177 );
6178 assert_eq!(
6179 root.read("/repo/HEAD", 64).await.expect("lazy child"),
6180 Bytes::from_static(b"ref: refs/heads/main\n")
6181 );
6182 }
6183
6184 #[tokio::test]
6185 async fn exact_promotion_accepts_an_identity_preserving_direct_write() {
6186 let fs = Fs::memory();
6187 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6188 let root = LazyWorkspace::attach(
6189 &fs,
6190 "root",
6191 Arc::clone(&source),
6192 MemoryLazyWorkspaceStore::default(),
6193 )
6194 .await
6195 .expect("attach");
6196 let source_node = match root.lookup("/file.txt").await.expect("source lookup") {
6197 LazyLookup::Source(node) => node,
6198 LazyLookup::Authored { .. } | LazyLookup::Shadow { .. } => {
6199 panic!("expected source node")
6200 }
6201 };
6202 let expected_source = root.source_file_id(&source_node);
6203 root.write("/file.txt", Bytes::from_static(b"replacement"))
6204 .await
6205 .expect("authored replacement");
6206
6207 root.promote_exact(
6208 "/file.txt",
6209 expected_source,
6210 64,
6211 IdempotencyKey::from_bytes([0x73; 16]),
6212 )
6213 .await
6214 .expect("identity-preserving write is already promoted");
6215 assert_eq!(
6216 root.read("/file.txt", 64)
6217 .await
6218 .expect("replacement remains"),
6219 Bytes::from_static(b"replacement")
6220 );
6221 }
6222
6223 #[tokio::test]
6224 async fn authored_state_and_tombstones_mask_the_source() {
6225 let fs = Fs::memory();
6226 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6227 let root = LazyWorkspace::attach(
6228 &fs,
6229 "root",
6230 Arc::clone(&source),
6231 MemoryLazyWorkspaceStore::default(),
6232 )
6233 .await
6234 .expect("attach");
6235
6236 root.write("/file.txt", Bytes::from_static(b"authored"))
6237 .await
6238 .expect("write");
6239 assert_eq!(
6240 root.read("/file.txt", 32).await.expect("authored read"),
6241 Bytes::from_static(b"authored")
6242 );
6243 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 1);
6244 root.remove("/file.txt").await.expect("remove");
6245 assert!(matches!(
6246 root.stat("/file.txt").await,
6247 Err(LazyWorkspaceError::NotFound)
6248 ));
6249 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 1);
6250 }
6251
6252 #[tokio::test]
6253 async fn source_epoch_can_rebind_without_enumeration() {
6254 let fs = Fs::memory();
6255 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6256 let root = LazyWorkspace::attach(
6257 &fs,
6258 "root",
6259 Arc::clone(&source),
6260 MemoryLazyWorkspaceStore::default(),
6261 )
6262 .await
6263 .expect("attach");
6264 root.stat("/observed.txt").await.expect("observe");
6265 let before = root.snapshot().await.expect("snapshot before rebind");
6266 source.invalidate();
6267 assert!(matches!(
6268 root.stat("/new.txt").await,
6269 Err(LazyWorkspaceError::StaleSource)
6270 ));
6271 let rebound = root.rebind_source().await.expect("rebind");
6272 assert_eq!(rebound.source, source.reference());
6273 assert_eq!(source.counts.pages.load(Ordering::Relaxed), 0);
6274 assert_eq!(
6275 root.stat("/observed.txt")
6276 .await
6277 .expect("cached")
6278 .logical_bytes,
6279 Some(6)
6280 );
6281 let after = root.snapshot().await.expect("snapshot after refresh");
6282 assert_ne!(after, before);
6283 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 2);
6284 }
6285
6286 #[tokio::test]
6287 async fn snapshot_exactification_uses_live_epoch_for_unresolved_paths() {
6288 let fs = Fs::memory();
6289 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6290 let root = LazyWorkspace::attach(
6291 &fs,
6292 "root",
6293 Arc::clone(&source),
6294 MemoryLazyWorkspaceStore::default(),
6295 )
6296 .await
6297 .expect("attach");
6298 let snapshot = root.snapshot().await.expect("snapshot before rebind");
6299
6300 source.replace(Bytes::from_static(b"changed"));
6301 root.rebind_source().await.expect("rebind");
6302 let exact = root
6303 .exactify_snapshot(
6304 snapshot,
6305 "historical-exact",
6306 IdempotencyKey::from_bytes([23; 16]),
6307 WorkBudget::UNBOUNDED,
6308 &CancellationToken::new(),
6309 )
6310 .await
6311 .expect("exactify historical snapshot");
6312
6313 assert_eq!(
6314 exact
6315 .value
6316 .read("/file.txt", 32)
6317 .await
6318 .expect("unresolved file from live epoch"),
6319 Bytes::from_static(b"changed")
6320 );
6321 }
6322
6323 #[tokio::test]
6324 async fn snapshot_exactification_keeps_observed_paths_on_the_pinned_epoch() {
6325 let fs = Fs::memory();
6326 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6327 let root = LazyWorkspace::attach(
6328 &fs,
6329 "root",
6330 Arc::clone(&source),
6331 MemoryLazyWorkspaceStore::default(),
6332 )
6333 .await
6334 .expect("attach");
6335 root.stat("/file.txt").await.expect("observe old epoch");
6336 let snapshot = root.snapshot().await.expect("snapshot after observation");
6337
6338 source.replace(Bytes::from_static(b"changed"));
6339 root.rebind_source().await.expect("rebind");
6340 let exact = root
6341 .exactify_snapshot(
6342 snapshot,
6343 "pinned-exact",
6344 IdempotencyKey::from_bytes([24; 16]),
6345 WorkBudget::UNBOUNDED,
6346 &CancellationToken::new(),
6347 )
6348 .await
6349 .expect("exactify pinned observation");
6350
6351 assert_eq!(
6352 exact
6353 .value
6354 .read("/file.txt", 32)
6355 .await
6356 .expect("observed file from pinned epoch"),
6357 Bytes::from_static(b"source")
6358 );
6359 }
6360
6361 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
6362 #[tokio::test]
6363 async fn native_source_rebind_refreshes_an_observed_file() {
6364 use crate::demand::native::NativeDemandSource;
6365 use crate::model::{FilesystemProfile, VolumeLimits};
6366
6367 let directory = tempfile::tempdir().expect("temporary source");
6368 let path = directory.path().join("file.txt");
6369 std::fs::write(&path, b"before").expect("initial file");
6370 let source = Arc::new(
6371 NativeDemandSource::open(
6372 directory.path(),
6373 FilesystemProfile::Portable,
6374 VolumeLimits::default(),
6375 )
6376 .await
6377 .expect("source"),
6378 );
6379 let fs = Fs::memory();
6380 let root = LazyWorkspace::attach(
6381 &fs,
6382 "native-rebind",
6383 Arc::clone(&source),
6384 MemoryLazyWorkspaceStore::default(),
6385 )
6386 .await
6387 .expect("attach");
6388 assert_eq!(
6389 root.read("/file.txt", 64).await.expect("initial read"),
6390 Bytes::from_static(b"before")
6391 );
6392 let before = root.snapshot().await.expect("snapshot before change");
6393
6394 std::fs::write(&path, b"after").expect("external write");
6395 source.invalidate();
6396 root.rebind_source().await.expect("rebind source");
6397
6398 assert_eq!(
6399 root.read("/file.txt", 64).await.expect("refreshed read"),
6400 Bytes::from_static(b"after")
6401 );
6402 assert_ne!(
6403 root.snapshot().await.expect("snapshot after change"),
6404 before
6405 );
6406 }
6407
6408 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
6409 #[tokio::test]
6410 async fn exactify_is_bounded_and_returns_only_a_complete_generation() {
6411 use crate::demand::native::NativeDemandSource;
6412 use crate::model::{FilesystemProfile, VolumeLimits};
6413
6414 let directory = tempfile::tempdir().expect("temporary source");
6415 std::fs::create_dir(directory.path().join("nested")).expect("nested directory");
6416 std::fs::write(directory.path().join("a.txt"), b"alpha").expect("first file");
6417 std::fs::write(directory.path().join("nested").join("b.txt"), b"beta")
6418 .expect("second file");
6419 let source = Arc::new(
6420 NativeDemandSource::open(
6421 directory.path(),
6422 FilesystemProfile::Portable,
6423 VolumeLimits::default(),
6424 )
6425 .await
6426 .expect("source"),
6427 );
6428 let fs = Fs::memory();
6429 let root =
6430 LazyWorkspace::attach(&fs, "exactify", source, MemoryLazyWorkspaceStore::default())
6431 .await
6432 .expect("attach");
6433 let mut insufficient = WorkBudget::UNBOUNDED;
6434 insufficient.source_entries_visited = 1;
6435 assert!(matches!(
6436 root.exactify(insufficient, &CancellationToken::new()).await,
6437 Err(LazyWorkspaceError::Work(_))
6438 ));
6439
6440 let exact = root
6441 .exactify(WorkBudget::UNBOUNDED, &CancellationToken::new())
6442 .await
6443 .expect("exact capture");
6444 assert_eq!(
6445 exact
6446 .value
6447 .read("/nested/b.txt", 16)
6448 .await
6449 .expect("captured file"),
6450 Bytes::from_static(b"beta")
6451 );
6452 assert_eq!(exact.work.source_entries_visited, 3);
6453 assert_eq!(
6454 exact.work.source_bytes_read, 9,
6455 "version-pinned demand is read once and measured by the blob builder"
6456 );
6457 }
6458
6459 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
6460 #[tokio::test]
6461 async fn exactify_subtree_keeps_siblings_lazy() {
6462 use crate::demand::native::NativeDemandSource;
6463 use crate::model::{FilesystemProfile, VolumeLimits};
6464
6465 let directory = tempfile::tempdir().expect("temporary source");
6466 std::fs::create_dir(directory.path().join("hot")).expect("hot directory");
6467 std::fs::create_dir(directory.path().join("cold")).expect("cold directory");
6468 std::fs::write(directory.path().join("hot/file.txt"), b"hot").expect("hot file");
6469 std::fs::write(directory.path().join("cold/file.txt"), b"cold").expect("cold file");
6470 let source = Arc::new(
6471 NativeDemandSource::open(
6472 directory.path(),
6473 FilesystemProfile::Portable,
6474 VolumeLimits::default(),
6475 )
6476 .await
6477 .expect("source"),
6478 );
6479 let root = LazyWorkspace::attach(
6480 &Fs::memory(),
6481 "subtree-exactify",
6482 source,
6483 MemoryLazyWorkspaceStore::default(),
6484 )
6485 .await
6486 .expect("attach");
6487
6488 let exact = root
6489 .exactify_subtree_with_permit(
6490 "/hot",
6491 WorkBudget::UNBOUNDED,
6492 &CancellationToken::new(),
6493 crate::PublicationPermit::Unrestricted,
6494 )
6495 .await
6496 .expect("capture hot subtree");
6497 assert_eq!(
6498 exact
6499 .value
6500 .read("/hot/file.txt", 16)
6501 .await
6502 .expect("hot content"),
6503 Bytes::from_static(b"hot")
6504 );
6505 #[cfg(feature = "native-mount")]
6506 {
6507 let staged = tempfile::tempdir().expect("empty native stage");
6508 exact
6509 .value
6510 .materialize_path(
6511 "/hot",
6512 &crate::MaterializeOptions::native(staged.path()),
6513 WorkBudget::UNBOUNDED,
6514 &CancellationToken::new(),
6515 )
6516 .await
6517 .expect("materialize only pinned hot subtree");
6518 assert_eq!(
6519 std::fs::read(staged.path().join("hot/file.txt")).expect("staged hot file"),
6520 b"hot"
6521 );
6522 assert!(!staged.path().join("cold").exists());
6523 }
6524 assert!(matches!(
6525 exact.value.read("/cold/file.txt", 16).await,
6526 Err(WorkspaceError::NotFound)
6527 ));
6528 assert!(matches!(
6529 root.lookup("/cold/file.txt")
6530 .await
6531 .expect("cold remains lazy"),
6532 LazyLookup::Source(_)
6533 ));
6534 assert!(matches!(
6535 root.exactify_subtree_with_permit(
6536 "/hot/file.txt",
6537 WorkBudget::UNBOUNDED,
6538 &CancellationToken::new(),
6539 crate::PublicationPermit::Unrestricted,
6540 )
6541 .await,
6542 Err(LazyWorkspaceError::NotDirectory)
6543 ));
6544 }
6545
6546 #[cfg(all(windows, feature = "native-watch"))]
6547 #[tokio::test]
6548 async fn source_page_batch_lookup_transcodes_windows_names_to_workspace_profile() {
6549 use crate::demand::native::NativeDemandSource;
6550 use crate::model::{FilesystemProfile, VolumeLimits};
6551
6552 let directory = tempfile::tempdir().expect("temporary source");
6553 std::fs::write(directory.path().join("café.txt"), b"source").expect("source file");
6554 let source = Arc::new(
6555 NativeDemandSource::open(
6556 directory.path(),
6557 FilesystemProfile::Windows,
6558 VolumeLimits::default(),
6559 )
6560 .await
6561 .expect("Windows source"),
6562 );
6563 let root = LazyWorkspace::attach(
6564 &Fs::memory(),
6565 "windows-source-portable-authored",
6566 source,
6567 MemoryLazyWorkspaceStore::default(),
6568 )
6569 .await
6570 .expect("attach portable authored workspace");
6571 assert_eq!(root.workspace().profile(), FilesystemProfile::Portable);
6572 let exact = root
6573 .exactify(WorkBudget::UNBOUNDED, &CancellationToken::new())
6574 .await
6575 .expect("exactify Windows source into portable authored workspace");
6576 assert_eq!(
6577 exact
6578 .value
6579 .read("/café.txt", 16)
6580 .await
6581 .expect("captured file"),
6582 Bytes::from_static(b"source")
6583 );
6584 }
6585
6586 #[cfg(all(feature = "native-watch", not(target_arch = "wasm32")))]
6587 #[tokio::test]
6588 async fn exactify_preserves_hard_links_across_batch_boundaries() {
6589 use crate::demand::native::NativeDemandSource;
6590 use crate::model::{FilesystemProfile, VolumeLimits};
6591
6592 let directory = tempfile::tempdir().expect("temporary source");
6593 let source_path = directory.path().join("a000");
6594 std::fs::write(&source_path, b"shared").expect("source file");
6595 for index in 1..520 {
6596 std::fs::hard_link(&source_path, directory.path().join(format!("a{index:03}")))
6597 .expect("source hard link");
6598 }
6599 let source = Arc::new(
6600 NativeDemandSource::open(
6601 directory.path(),
6602 FilesystemProfile::Portable,
6603 VolumeLimits::default(),
6604 )
6605 .await
6606 .expect("source"),
6607 );
6608 let root = LazyWorkspace::attach(
6609 &Fs::memory(),
6610 "hard-link-batches",
6611 source,
6612 MemoryLazyWorkspaceStore::default(),
6613 )
6614 .await
6615 .expect("attach");
6616 let exact = root
6617 .exactify(WorkBudget::UNBOUNDED, &CancellationToken::new())
6618 .await
6619 .expect("exactify all batches");
6620 let first = exact.value.stat("/a000").await.expect("first stat");
6621 let last = exact.value.stat("/a519").await.expect("last stat");
6622 assert_eq!(first.file_id, last.file_id);
6623 assert_eq!(last.link_count, 520);
6624 }
6625
6626 #[tokio::test]
6627 async fn directory_pages_emit_authored_entries_once_after_source_phase() {
6628 let fs = Fs::memory();
6629 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6630 let root = LazyWorkspace::attach(
6631 &fs,
6632 "root",
6633 Arc::clone(&source),
6634 MemoryLazyWorkspaceStore::default(),
6635 )
6636 .await
6637 .expect("attach");
6638 root.write("/file.txt", Bytes::from_static(b"authored"))
6639 .await
6640 .expect("write");
6641
6642 let source_page = root
6643 .list_directory("/", None, 16)
6644 .await
6645 .expect("source phase");
6646 assert!(source_page.entries.is_empty());
6647 let authored_page = root
6648 .list_directory("/", source_page.next, 16)
6649 .await
6650 .expect("authored phase");
6651 assert_eq!(authored_page.entries.len(), 1);
6652 assert!(authored_page.entries[0].authored);
6653 assert!(authored_page.next.is_none());
6654 }
6655
6656 #[tokio::test]
6657 async fn directory_cursor_rejects_overlay_changes_between_pages() {
6658 let fs = Fs::memory();
6659 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6660 let root = LazyWorkspace::attach(
6661 &fs,
6662 "root",
6663 Arc::clone(&source),
6664 MemoryLazyWorkspaceStore::default(),
6665 )
6666 .await
6667 .expect("attach");
6668 let first = root
6669 .list_directory("/", None, 16)
6670 .await
6671 .expect("first page");
6672 root.stat("/newly-observed.txt").await.expect("observe");
6673
6674 assert!(matches!(
6675 root.list_directory("/", first.next, 16).await,
6676 Err(LazyWorkspaceError::StaleCursor)
6677 ));
6678 }
6679
6680 #[tokio::test]
6681 async fn failed_authored_remove_rolls_back_source_tombstone() {
6682 let fs = Fs::memory();
6683 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6684 let root = LazyWorkspace::attach(
6685 &fs,
6686 "root",
6687 Arc::clone(&source),
6688 MemoryLazyWorkspaceStore::default(),
6689 )
6690 .await
6691 .expect("attach");
6692 root.write("/directory/file.txt", Bytes::from_static(b"authored"))
6693 .await
6694 .expect("write");
6695 let before = root.state().await.expect("state");
6696
6697 assert!(matches!(
6698 root.remove("/directory").await,
6699 Err(LazyWorkspaceError::Workspace(_))
6700 ));
6701 let after = root.state().await.expect("state after failure");
6702 assert_eq!(after.overlay, before.overlay);
6703 assert_eq!(after.shadows, before.shadows);
6704 assert!(after.pending_remove.is_none());
6705 assert!(root.stat("/directory").await.expect("directory").authored);
6706 }
6707
6708 #[tokio::test]
6709 async fn open_migrates_released_schema_five_state() {
6710 let fs = Fs::memory();
6711 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6712 let store = MemoryLazyWorkspaceStore::default();
6713 let root = LazyWorkspace::attach(&fs, "root", Arc::clone(&source), store.clone())
6714 .await
6715 .expect("attach");
6716 root.write("/file.txt", Bytes::from_static(b"authored"))
6717 .await
6718 .expect("write");
6719 let prior = root.state().await.expect("state");
6720 let mut idle = serde_json::to_value(&prior).expect("encode");
6721 idle["schema_version"] = LAZY_STATE_SCHEMA_V5.into();
6722 assert_eq!(
6723 serde_json::from_value::<LazyWorkspaceState>(idle).expect("idle schema 5"),
6724 prior
6725 );
6726
6727 let LazyLookup::Authored { stat, .. } = root.lookup("/file.txt").await.expect("authored")
6728 else {
6729 panic!("expected authored file");
6730 };
6731 let record = root
6732 .workspace()
6733 .record_by_id(stat.file_id)
6734 .await
6735 .expect("authored record");
6736 let staged_shadows = root
6737 .insert_shadow(prior.shadows, stat.file_id, record, stat.metadata)
6738 .await
6739 .expect("staged identity shadow");
6740 let tombstone = root
6741 .insert_overlay(
6742 prior.overlay,
6743 "/file.txt".to_owned(),
6744 LazyOverlayChange::Tombstone,
6745 )
6746 .await
6747 .expect("tombstone");
6748 let mut pending = serde_json::to_value(LazyWorkspaceState {
6749 revision: prior.revision + 1,
6750 overlay: tombstone,
6751 shadows: staged_shadows,
6752 pending_remove: None,
6753 ..prior.clone()
6754 })
6755 .expect("encode");
6756 pending["schema_version"] = LAZY_STATE_SCHEMA_V5.into();
6757 pending["pending_remove"] = serde_json::json!({
6758 "path": "/file.txt",
6759 "prior_overlay": prior.overlay,
6760 "tombstone_overlay": tombstone,
6761 "kind": PendingLazyRemoveKind::Authored {
6762 idempotency_key: IdempotencyKey::from_bytes([0x35; 16]),
6763 },
6764 });
6765 let pending = serde_json::from_value::<LazyWorkspaceState>(pending).expect("schema 5");
6766 assert!(
6767 store
6768 .compare_and_swap_lazy_workspace(root.workspace().id(), prior.revision, pending)
6769 .await
6770 .expect("prepare")
6771 );
6772
6773 let reopened = LazyWorkspace::open(
6774 fs.open_workspace("root").await.expect("workspace"),
6775 source,
6776 store,
6777 )
6778 .await
6779 .expect("reopen migrated state");
6780 let recovered = reopened.state().await.expect("recovered state");
6781 assert_eq!(recovered.schema_version, LAZY_STATE_SCHEMA);
6782 assert_eq!(recovered.overlay, prior.overlay);
6783 assert_eq!(recovered.shadows, staged_shadows);
6784 assert!(recovered.pending_remove.is_none());
6785 assert!(reopened.stat("/file.txt").await.expect("file").authored);
6786 }
6787
6788 #[tokio::test]
6789 async fn open_recovers_remove_intent_from_authored_state() {
6790 let fs = Fs::memory();
6791 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6792 let store = MemoryLazyWorkspaceStore::default();
6793 let root = LazyWorkspace::attach(&fs, "root", Arc::clone(&source), store.clone())
6794 .await
6795 .expect("attach");
6796 root.write("/file.txt", Bytes::from_static(b"authored"))
6797 .await
6798 .expect("write");
6799 let prior = root.state().await.expect("state");
6800 let LazyLookup::Authored { stat, .. } = root.lookup("/file.txt").await.expect("authored")
6801 else {
6802 panic!("expected authored file");
6803 };
6804 let record = root
6805 .workspace()
6806 .record_by_id(stat.file_id)
6807 .await
6808 .expect("authored record");
6809 let staged_shadows = root
6810 .insert_shadow(prior.shadows, stat.file_id, record, stat.metadata)
6811 .await
6812 .expect("staged identity shadow");
6813 assert_ne!(staged_shadows, prior.shadows);
6814 let tombstone = root
6815 .insert_overlay(
6816 prior.overlay,
6817 "/file.txt".to_owned(),
6818 LazyOverlayChange::Tombstone,
6819 )
6820 .await
6821 .expect("tombstone");
6822 let first_key = IdempotencyKey::from_bytes([0x31; 16]);
6823 let pending = LazyWorkspaceState {
6824 revision: prior.revision + 1,
6825 overlay: tombstone,
6826 shadows: staged_shadows,
6827 pending_remove: Some(PendingLazyRemove {
6828 prior_overlay: prior.overlay,
6829 prior_shadows: prior.shadows,
6830 prepared_overlay: tombstone,
6831 kind: PendingLazyRemoveKind::Authored {
6832 idempotency_key: first_key,
6833 },
6834 }),
6835 ..prior.clone()
6836 };
6837 assert!(
6838 store
6839 .compare_and_swap_lazy_workspace(root.workspace().id(), prior.revision, pending)
6840 .await
6841 .expect("prepare")
6842 );
6843
6844 let reopened = LazyWorkspace::open(
6845 fs.open_workspace("root").await.expect("workspace"),
6846 Arc::clone(&source),
6847 store.clone(),
6848 )
6849 .await
6850 .expect("recover before physical remove");
6851 let recovered = reopened.state().await.expect("recovered state");
6852 assert_eq!(recovered.overlay, prior.overlay);
6853 assert_eq!(recovered.shadows, prior.shadows);
6854 assert!(recovered.pending_remove.is_none());
6855
6856 let committed_key = IdempotencyKey::from_bytes([0x32; 16]);
6857 let prepared = LazyWorkspaceState {
6858 revision: recovered.revision + 1,
6859 overlay: tombstone,
6860 shadows: staged_shadows,
6861 pending_remove: Some(PendingLazyRemove {
6862 prior_overlay: recovered.overlay,
6863 prior_shadows: recovered.shadows,
6864 prepared_overlay: tombstone,
6865 kind: PendingLazyRemoveKind::Authored {
6866 idempotency_key: committed_key,
6867 },
6868 }),
6869 ..recovered.clone()
6870 };
6871 assert!(
6872 store
6873 .compare_and_swap_lazy_workspace(
6874 reopened.workspace().id(),
6875 recovered.revision,
6876 prepared
6877 )
6878 .await
6879 .expect("prepare second")
6880 );
6881 let mut removal = reopened
6882 .workspace()
6883 .begin_transaction(committed_key)
6884 .await
6885 .expect("removal transaction");
6886 removal.remove("/file.txt").await.expect("stage remove");
6887 assert!(matches!(
6888 removal.commit().await.expect("physical remove"),
6889 TransactionCommit::Committed(_)
6890 ));
6891
6892 let finalized = LazyWorkspace::open(
6893 fs.open_workspace("root").await.expect("workspace"),
6894 Arc::clone(&source),
6895 store,
6896 )
6897 .await
6898 .expect("recover after physical remove");
6899 let final_state = finalized.state().await.expect("final state");
6900 assert_eq!(final_state.overlay, tombstone);
6901 assert_eq!(final_state.shadows, staged_shadows);
6902 assert!(matches!(
6903 finalized.stat("/file.txt").await,
6904 Err(LazyWorkspaceError::NotFound)
6905 ));
6906 }
6907
6908 #[tokio::test]
6909 async fn source_only_remove_recovery_keeps_tombstone_across_a_competing_create() {
6910 let fs = Fs::memory();
6911 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6912 let store = MemoryLazyWorkspaceStore::default();
6913 let root = LazyWorkspace::attach(&fs, "root", Arc::clone(&source), store.clone())
6914 .await
6915 .expect("attach");
6916 root.stat("/file.txt").await.expect("observe source");
6917 let prior = root.state().await.expect("state");
6918 let tombstone = root
6919 .insert_overlay(
6920 prior.overlay,
6921 "/file.txt".to_owned(),
6922 LazyOverlayChange::Tombstone,
6923 )
6924 .await
6925 .expect("tombstone");
6926 let pending = LazyWorkspaceState {
6927 revision: prior.revision + 1,
6928 overlay: tombstone,
6929 pending_remove: Some(PendingLazyRemove {
6930 prior_overlay: prior.overlay,
6931 prior_shadows: prior.shadows,
6932 prepared_overlay: tombstone,
6933 kind: PendingLazyRemoveKind::SourceOnly,
6934 }),
6935 ..prior.clone()
6936 };
6937 assert!(
6938 store
6939 .compare_and_swap_lazy_workspace(root.workspace().id(), prior.revision, pending)
6940 .await
6941 .expect("prepare")
6942 );
6943 root.workspace()
6944 .write("/file.txt", Bytes::from_static(b"authored"))
6945 .await
6946 .expect("competing authored create");
6947
6948 let reopened = LazyWorkspace::open(
6949 fs.open_workspace("root").await.expect("workspace"),
6950 Arc::clone(&source),
6951 store,
6952 )
6953 .await
6954 .expect("recover source removal");
6955 assert_eq!(reopened.state().await.expect("state").overlay, tombstone);
6956 assert_eq!(
6957 reopened
6958 .read("/file.txt", 64)
6959 .await
6960 .expect("authored replacement"),
6961 Bytes::from_static(b"authored")
6962 );
6963 reopened.remove("/file.txt").await.expect("remove authored");
6964 assert!(matches!(
6965 reopened.stat("/file.txt").await,
6966 Err(LazyWorkspaceError::NotFound)
6967 ));
6968 }
6969
6970 #[tokio::test]
6971 async fn observation_index_stays_balanced_and_nodes_stay_constant_size() {
6972 let fs = Fs::memory();
6973 let source = Arc::new(CountingSource::new(Bytes::from_static(b"source")));
6974 let store = MemoryLazyWorkspaceStore::default();
6975 let root = LazyWorkspace::attach(&fs, "root", Arc::clone(&source), store.clone())
6976 .await
6977 .expect("attach");
6978 for index in 0..1_000 {
6979 root.stat(&format!("/paths/{index:04}.txt"))
6980 .await
6981 .expect("observe");
6982 }
6983 let root_id = root.state().await.expect("state").overlay;
6984 let state = store
6985 .inner
6986 .lock()
6987 .unwrap_or_else(|poisoned| poisoned.into_inner());
6988 fn depth(overlays: &BTreeMap<LazyOverlayId, LazyOverlay>, id: LazyOverlayId) -> usize {
6989 match &overlays[&id] {
6990 LazyOverlay::Empty => 0,
6991 LazyOverlay::Node { left, right, .. } => {
6992 1 + depth(overlays, *left).max(depth(overlays, *right))
6993 }
6994 }
6995 }
6996 assert!(depth(&state.overlays, root_id) < 64);
6997 assert!(state.overlays.values().all(|overlay| {
6998 serde_json::to_vec(overlay)
6999 .expect("encode overlay node")
7000 .len()
7001 < 2_048
7002 }));
7003 assert_eq!(source.counts.lookups.load(Ordering::Relaxed), 1_000);
7004 }
7005
7006 #[test]
7007 fn million_path_exactify_capture_memory_is_exactly_admitted() {
7008 const PATHS: u64 = 1_000_000;
7009
7010 fn capture(budget: WorkBudget) -> Result<(WorkCounters, u64), LazyWorkspaceError> {
7011 let mut paths = Vec::new();
7012 let mut retained = 0_u64;
7013 let mut work = WorkCounters::default();
7014 for index in 0..PATHS {
7015 let path = format!("/wide/{index:06}.txt");
7016 reserve_exactify_path_slot(&mut paths, &mut retained, &mut work, budget)?;
7017 retain_exactify_string(&path, &mut retained, &mut work, budget)?;
7018 paths.push(path);
7019 }
7020 let expected = exactify_path_vector_bytes(&paths)?
7021 + paths
7022 .iter()
7023 .map(|path| u64::try_from(path.capacity()).unwrap_or(u64::MAX))
7024 .sum::<u64>();
7025 assert_eq!(retained, expected);
7026 Ok((work, retained))
7027 }
7028
7029 let (work, retained) = capture(WorkBudget::UNBOUNDED).expect("unbounded capture");
7030 assert!(work.peak_allocation_bytes >= retained);
7031 assert!(work.allocation_operations >= PATHS);
7032 let mut insufficient = WorkBudget::UNBOUNDED;
7033 insufficient.peak_allocation_bytes = work.peak_allocation_bytes - 1;
7034 assert!(matches!(
7035 capture(insufficient),
7036 Err(LazyWorkspaceError::Work(_))
7037 ));
7038 }
7039}