pub struct ContentAddressedMount<S: ObjectStore + 'static = AnyStore> { /* private fields */ }Expand description
In-mount overlay: a snapshot-time view of the parent state plus pending writes the agent has issued since.
Writes never modify the immutable state; they accumulate in
[Pending] until ContentAddressedMount::capture folds them
into a fresh state.
Implementations§
Source§impl<S: ObjectStore + 'static> ContentAddressedMount<S>
impl<S: ObjectStore + 'static> ContentAddressedMount<S>
Sourcepub fn new(
repo: Repository<RefManager, OpLog, S>,
thread: impl Into<String>,
) -> Result<Self>
pub fn new( repo: Repository<RefManager, OpLog, S>, thread: impl Into<String>, ) -> Result<Self>
Open a writable mount of thread against repo.
Resolves the thread once, up front, so every subsequent
lookup/read walks from a fixed snapshot. Writes accumulate
in the pending tier until Self::capture folds them into a
new state. To advance to a newer state, call Self::refresh.
Equivalent to Self::with_options with default options:
a fresh per-mount blob cache. Daemon callers that want
cross-mount cache reuse should construct an
Arc<BlobCachePool> once and use with_options instead.
Sourcepub fn with_options(
repo: Repository<RefManager, OpLog, S>,
thread: impl Into<String>,
options: MountOptions,
) -> Result<Self>
pub fn with_options( repo: Repository<RefManager, OpLog, S>, thread: impl Into<String>, options: MountOptions, ) -> Result<Self>
Construct a mount with explicit options. Lets the caller share
a blob cache across mounts in the same process — see
MountOptions::blob_cache.
Sourcepub fn blob_cache_pool(&self) -> &Arc<BlobCachePool>
pub fn blob_cache_pool(&self) -> &Arc<BlobCachePool>
Borrow the shared blob cache pool. Useful when the caller
wants to spawn a BlobCachePool-aware pre-warmer or
inspect cache stats.
Sourcepub fn with_promotion_policy(self, policy: PromotionPolicy) -> Self
pub fn with_promotion_policy(self, policy: PromotionPolicy) -> Self
Override the promotion policy. Re-spawns (or terminates) the
safety-sweep worker to honour the new sweep_interval.
Mostly useful for tests that want a tight idle window or to
disable idle-promotion entirely.
Sourcepub fn refresh(&self) -> Result<()>
pub fn refresh(&self) -> Result<()>
Re-resolve the thread and adopt the new state. Existing inodes are not invalidated — callers who want a clean slate should drop the mount and recreate.
Sourcepub fn current_change_id(&self) -> ChangeId
pub fn current_change_id(&self) -> ChangeId
The change id this mount currently points at.
Sourcepub fn clear_blob_cache(&self)
pub fn clear_blob_cache(&self)
Drop every cached blob — both the mount-side LRU and the
underlying ObjectStore’s recent_blobs/recent_trees
caches. The next read on each blob pays full I/O +
decompression cost. Exposed for benchmarks that want to
measure the true cold-cache path without rebuilding the
whole mount.
Sourcepub fn prewarm(&self) -> PrewarmHandle
pub fn prewarm(&self) -> PrewarmHandle
Spawn a background tree-walker that hydrates every file blob
in the captured tree into the shared blob cache. The first
kernel read after this finishes is served from memory at
Arc::clone + memcpy cost — beats std::fs::read on every
tier we benchmark.
The returned PrewarmHandle is the caller’s lever:
- Drop it without calling anything → the prewarmer keeps
running until natural completion or the mount drops
(the workers hold
Weak<MountInner>and self-terminate when the strong count hits zero). .cancel()signals shutdown without joining..wait()joins all workers and returns the final stats.
Workers stop early when the cache is ≥ 90% full to avoid churn-evicting work they just did. Blobs already cached (from a sibling mount sharing the same pool) are skipped cheaply — this is the fork-thread fast path.
Sourcepub fn lookup_path(&self, path: impl AsRef<Path>) -> Result<NodeId>
pub fn lookup_path(&self, path: impl AsRef<Path>) -> Result<NodeId>
Resolve a mount-relative path to a NodeId. Used by tests
that don’t go through lookup step-by-step.
Sourcepub fn flush_node(&self, node: NodeId) -> Result<()>
pub fn flush_node(&self, node: NodeId) -> Result<()>
Promote the hot buffer for node (if any) to a CAS blob and
record it in the pending tree. Routed from the FUSE flush
callback (per-descriptor-close). Orphaned nodes deliberately
do nothing here — see [MountInner::flush_node] for the
lifecycle rationale.
Sourcepub fn release_node(&self, node: NodeId) -> Result<()>
pub fn release_node(&self, node: NodeId) -> Result<()>
Final close of node from a FUSE release callback. Decrements
the open-handle refcount; on the last close, drops orphan
state and (for non-orphans) promotes any surviving hot buffer.
Sourcepub fn on_open(&self, node: NodeId) -> Result<()>
pub fn on_open(&self, node: NodeId) -> Result<()>
Notify the mount that a new open handle for node was minted
(FUSE open / create callback). Used to time the orphan
cleanup against the final close (see
Self::release_node / [MountInner::release_node]).
Bumps the open count on the existing NodeState, minting a
Live { open_count: 1 } entry if the node is untracked. An
Orphan can also be opened (rare — only via an fh the kernel
still holds across a re-lookup race); we bump its refcount so
the final release fires correctly.
Sourcepub fn unlink_path(&self, path: impl AsRef<Path>) -> Result<()>
pub fn unlink_path(&self, path: impl AsRef<Path>) -> Result<()>
Mark path as deleted in the pending tier. Subsequent
lookup/enumerate calls will skip the underlying captured
entry, and capture() will fold the deletion into the new
state’s tree (pruning empty parent dirs as needed).
Low-level (path-based) helper — unlike Self::unlink_entry
it does not honour POSIX open-unlinked semantics. Used by
tests that bypass the FUSE-callback lifecycle. The
NodeId-keyed buffers for the path’s current owner are dropped
(no orphan tracking).
Sourcepub fn create_file(
&self,
parent: NodeId,
name: &OsStr,
mode: FileMode,
exclusive: bool,
) -> Result<Entry>
pub fn create_file( &self, parent: NodeId, name: &OsStr, mode: FileMode, exclusive: bool, ) -> Result<Entry>
Open-or-create a regular file under parent, mirroring
open(O_CREAT[|O_EXCL]) from userspace.
When the named entry doesn’t exist, mints a fresh
[NodeRecord::PendingFile] inode + an empty hot buffer so the
new path is immediately visible to lookup /
attrs and the first
write drops cleanly into the existing
two-tier model.
When the named entry already exists:
exclusive=true⇒MountError::AlreadyExists(errnoEEXIST).exclusive=false⇒ returns the existing entry. The kernel follows up withsetattr(size=0)forO_TRUNCcallers, which we honour inset_attrs.
Sourcepub fn make_dir(&self, parent: NodeId, name: &OsStr) -> Result<Entry>
pub fn make_dir(&self, parent: NodeId, name: &OsStr) -> Result<Entry>
Create an empty directory under parent. Recorded as an
[Pending::explicit_dirs] entry so the new path is visible to
lookup/enumerate even when no child has been written yet.
Sourcepub fn unlink_entry(&self, parent: NodeId, name: &OsStr) -> Result<()>
pub fn unlink_entry(&self, parent: NodeId, name: &OsStr) -> Result<()>
Delete a regular file (or symlink) named name under parent.
POSIX open-unlinked semantics: the directory entry goes (path
tombstoned, inodes.by_path[path] retired), but if any fd
still references the inode, the bytes survive in hot[node] /
warm[node] until the final release. Under the post-spike
unified NodeId-keyed model
(docs/design/mount-posix-semantics.md §1.2 T1/T2), this is a
state transition only — no byte migration. Pre-spike code
dropped pending.hot[node_id] here (Codex thread 3293307302
r9) and migrated warm[path] into orphan_warm[node] (r8);
both steps go away.
Sourcepub fn rmdir_entry(&self, parent: NodeId, name: &OsStr) -> Result<()>
pub fn rmdir_entry(&self, parent: NodeId, name: &OsStr) -> Result<()>
Remove the empty directory name under parent. Fails with
ENOTEMPTY if any child resolves through the mount.
Sourcepub fn rename_entry(
&self,
old_parent: NodeId,
old_name: &OsStr,
new_parent: NodeId,
new_name: &OsStr,
) -> Result<()>
pub fn rename_entry( &self, old_parent: NodeId, old_name: &OsStr, new_parent: NodeId, new_name: &OsStr, ) -> Result<()>
Move (old_parent, old_name) to (new_parent, new_name).
Handles file + symlink renames across any pair of overlay /
captured paths, and overlay-only directory rename (a captured
directory rename would require recursively rewriting the
tombstone/warm map — out of scope for the cargo / git path).
Sourcepub fn rename_entry_with_options(
&self,
old_parent: NodeId,
old_name: &OsStr,
new_parent: NodeId,
new_name: &OsStr,
options: RenameOptions,
) -> Result<()>
pub fn rename_entry_with_options( &self, old_parent: NodeId, old_name: &OsStr, new_parent: NodeId, new_name: &OsStr, options: RenameOptions, ) -> Result<()>
Same as Self::rename_entry but honours RenameOptions.
no_replace (Linux RENAME_NOREPLACE) refuses the rename when
the destination already resolves; the check is performed inside
the same write-side critical section as the mutation, so a
concurrent writer cannot install the destination between the
check and the rename.
Sourcepub fn set_attrs(&self, node: NodeId, update: AttrUpdate) -> Result<Attrs>
pub fn set_attrs(&self, node: NodeId, update: AttrUpdate) -> Result<Attrs>
Apply attribute updates from a FUSE setattr / FSKit
setattr / etc. Returns post-update Attrs for an
inline reply.
Sourcepub fn create_symlink(
&self,
parent: NodeId,
name: &OsStr,
target: &Path,
) -> Result<Entry>
pub fn create_symlink( &self, parent: NodeId, name: &OsStr, target: &Path, ) -> Result<Entry>
Create a symbolic link under parent. Target bytes are kept
in the pending tier verbatim; capture writes them as a CAS
blob and emits a Symlink tree entry.
Sourcepub fn read_link(&self, node: NodeId) -> Result<OsString>
pub fn read_link(&self, node: NodeId) -> Result<OsString>
Read the target of a symlink node. Works for both overlay
(PendingSymlink) and captured (Symlink) records.
Codex r12 thread 3293510316 (P1): the prior implementation
used OsStr::from_encoded_bytes_unchecked on bytes loaded
from the object store, which is unsound — that API’s safety
contract requires bytes minted by OsStr::as_encoded_bytes
in this process and Rust version, but captured-tree blobs
can come from any process and version. The corrected path
delegates to [symlink_target_from_bytes], which uses
platform-safe APIs (OsStrExt::from_bytes on Unix, UTF-8
validation on Windows).
Source§impl ContentAddressedMount
impl ContentAddressedMount
Sourcepub fn capture(&self, intent: impl Into<Option<String>>) -> Result<ChangeId>
pub fn capture(&self, intent: impl Into<Option<String>>) -> Result<ChangeId>
Drain the pending tier into a fresh heddle state and update the thread to point at it.
This is the mount-side analogue of heddle capture/heddle snapshot: rather than walking a worktree to discover changed
files, it folds the in-memory pending map into a real
Tree object, records a State, and advances the
thread’s HEAD ref.
intent is propagated to state.intent. Attribution is
pulled from the repository’s default attribution path
(Repository::get_attribution) — this honours the
HEDDLE_AGENT_* env, the repo config, and the user’s
principal. Richer attribution paths (CLI overrides,
AgentRegistry, session segments) live in
crates/cli/src/cli/commands/snapshot.rs::build_attribution;
when the CLI wires this up it should call
Self::capture_with_attribution instead and pass the result
of that helper.
Sourcepub fn capture_with_attribution(
&self,
intent: impl Into<Option<String>>,
attribution: Attribution,
) -> Result<ChangeId>
pub fn capture_with_attribution( &self, intent: impl Into<Option<String>>, attribution: Attribution, ) -> Result<ChangeId>
Same as Self::capture but with caller-supplied attribution.
The CLI uses this so it can mirror build_attribution from
snapshot.rs (CLI overrides, agent registry lookup, etc.).
Trait Implementations§
Source§impl<S: ObjectStore + 'static> Drop for ContentAddressedMount<S>
impl<S: ObjectStore + 'static> Drop for ContentAddressedMount<S>
Source§impl<S: ObjectStore + 'static> PlatformShell for ContentAddressedMount<S>
impl<S: ObjectStore + 'static> PlatformShell for ContentAddressedMount<S>
Source§fn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>
fn lookup(&self, parent: NodeId, name: &OsStr) -> Result<Option<Entry>>
name inside parent. Returns None for ENOENT.Source§fn read(&self, node: NodeId, offset: u64, buf: &mut [u8]) -> Result<usize>
fn read(&self, node: NodeId, offset: u64, buf: &mut [u8]) -> Result<usize>
buf.len() bytes from node, starting at offset.
Returns the number of bytes actually written into buf.Source§fn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>
fn write(&self, node: NodeId, offset: u64, data: &[u8]) -> Result<usize>
data to node at offset. Returns bytes written.Source§fn invalidate(&self, node: NodeId) -> Result<()>
fn invalidate(&self, node: NodeId) -> Result<()>
node. The platform layer calls
this when the underlying state moves and previously-handed-out
inode numbers may now point at the wrong content.Source§fn flush(&self, node: NodeId) -> Result<()>
fn flush(&self, node: NodeId) -> Result<()>
node into a CAS blob. The
FUSE flush callback dispatches here (fires on close(2)
and explicit fsync). Default: no-op for read-only mounts. Read moreSource§fn release(&self, node: NodeId) -> Result<()>
fn release(&self, node: NodeId) -> Result<()>
node. The FUSE release callback dispatches
here; it fires once per open(2) after the last fd derived
from that open is closed. This is the canonical “last close of
the inode” signal — it is the right hook (NOT Self::flush)
for retiring per-inode lifecycle state like orphan-tracking
markers or open-handle refcounts. Default: identical to flush
so shells that do not maintain per-inode lifecycle state
inherit a uniform contract.Source§fn on_open(&self, node: NodeId) -> Result<()>
fn on_open(&self, node: NodeId) -> Result<()>
node has
been minted. FUSE adapters call this on the open / create
callbacks so the shell can maintain a per-inode open-handle
refcount — used to time the Self::release cleanup against
the final close instead of the first one. Default: no-op so
shells without lifecycle state are unaffected.Source§fn create_file(
&self,
parent: NodeId,
name: &OsStr,
mode: FileMode,
exclusive: bool,
) -> Result<Entry>
fn create_file( &self, parent: NodeId, name: &OsStr, mode: FileMode, exclusive: bool, ) -> Result<Entry>
Source§fn make_dir(&self, parent: NodeId, name: &OsStr) -> Result<Entry>
fn make_dir(&self, parent: NodeId, name: &OsStr) -> Result<Entry>
parent in the overlay.
Returns the new directory’s Entry. Fails with
MountError::AlreadyExists when name already resolves.Source§fn rmdir_entry(&self, parent: NodeId, name: &OsStr) -> Result<()>
fn rmdir_entry(&self, parent: NodeId, name: &OsStr) -> Result<()>
name under parent. Fails
with MountError::NotADirectory for a file, with
MountError::NotEmpty when the directory still has visible
children (across captured tree + pending tier), or
MountError::NotFound when nothing resolves.Source§fn rename_entry(
&self,
old_parent: NodeId,
old_name: &OsStr,
new_parent: NodeId,
new_name: &OsStr,
) -> Result<()>
fn rename_entry( &self, old_parent: NodeId, old_name: &OsStr, new_parent: NodeId, new_name: &OsStr, ) -> Result<()>
(old_parent, old_name) to
(new_parent, new_name). Handles both same-directory and
cross-directory cases. Replacing an existing entry of the
same kind is allowed (POSIX semantics); replacing a directory
with a file (or vice-versa) fails with
MountError::IsADirectory / MountError::NotADirectory.Source§fn rename_entry_with_options(
&self,
old_parent: NodeId,
old_name: &OsStr,
new_parent: NodeId,
new_name: &OsStr,
options: RenameOptions,
) -> Result<()>
fn rename_entry_with_options( &self, old_parent: NodeId, old_name: &OsStr, new_parent: NodeId, new_name: &OsStr, options: RenameOptions, ) -> Result<()>
Self::rename_entry but honours RenameOptions —
in particular no_replace, which atomically refuses the rename
when the destination already resolves. The check + the
directory-entry mutation MUST happen under a single critical
section to avoid a TOCTOU window between the existence check
and the rename itself. Default: ignore options and dispatch to
rename_entry (preserving the existing trait surface for
shells that do not yet support flags).Source§fn set_attrs(&self, node: NodeId, update: AttrUpdate) -> Result<Attrs>
fn set_attrs(&self, node: NodeId, update: AttrUpdate) -> Result<Attrs>
node. Returns the post-update
Attrs so callers can reply without a second getattr
round trip. See AttrUpdate for which fields the overlay
actually persists; unsupported fields are no-ops.