pub struct CacheEngine { /* private fields */ }Expand description
Engine bundling a filesystem-backed iroh-blobs store with an optional
origin backend. Lookups hit the store first; on miss and when an origin is
configured, bytes are pulled and BLAKE3-verified. Insert-before-return is a
property of the buffered path (Self::get / Self::populate), not of
this type: Self::admit_bao commits only a verified sub-range.
Implementations§
Source§impl CacheEngine
impl CacheEngine
Sourcepub async fn open(
cache_dir: &Path,
origins: Vec<Arc<dyn Origin>>,
max_blob_mb: u64,
) -> CacheResult<Self>
pub async fn open( cache_dir: &Path, origins: Vec<Arc<dyn Origin>>, max_blob_mb: u64, ) -> CacheResult<Self>
Open or create the store at cache_dir. max_blob_mb caps the size
of any single blob pulled from the origin. Oversize payloads typically
surface as CacheError::OriginError (the HTTP origin trips the cap
mid-stream before the engine sees the bytes), or as
CacheError::BlobTooLarge when a custom Origin impl returns bytes
that exceed the cap without self-enforcement.
Sourcepub async fn open_with_pinned(
cache_dir: &Path,
origins: Vec<Arc<dyn Origin>>,
max_blob_mb: u64,
pinned: PinnedHashes,
) -> CacheResult<Self>
pub async fn open_with_pinned( cache_dir: &Path, origins: Vec<Arc<dyn Origin>>, max_blob_mb: u64, pinned: PinnedHashes, ) -> CacheResult<Self>
Open the cache with an initial pinning set. The set is held in an
ArcSwap internally so subsequent SIGHUP reloads can call
Self::set_pinned without rebuilding the engine.
Defaults the retry policy to RetryPolicy::default, wires no
metrics handle, and disables periodic GC. Use Self::open_full to
override any of those — the node crate plugs in its
Arc<CacheMetrics>, the operator-configured retry policy, and the
cache.gc_interval_sec through that path.
Sourcepub async fn open_full(
cache_dir: &Path,
origins: Vec<Arc<dyn Origin>>,
max_blob_mb: u64,
pinned: PinnedHashes,
retry_policy: RetryPolicy,
circuit_breaker: CircuitBreakerPolicy,
metrics: Option<Arc<CacheMetrics>>,
gc_interval: Duration,
) -> CacheResult<Self>
pub async fn open_full( cache_dir: &Path, origins: Vec<Arc<dyn Origin>>, max_blob_mb: u64, pinned: PinnedHashes, retry_policy: RetryPolicy, circuit_breaker: CircuitBreakerPolicy, metrics: Option<Arc<CacheMetrics>>, gc_interval: Duration, ) -> CacheResult<Self>
Open the cache with full control over policy, metrics, and GC wiring.
The runtime’s build_cache uses this directly; tests usually want
Self::open or Self::open_with_pinned with their defaults.
gc_interval controls iroh-blobs’ built-in GC sweep loop (#518).
Duration::ZERO disables periodic GC; any other value is forwarded
to iroh_blobs::store::GcConfig and iroh-blobs spawns its own GC
task on its internal runtime. Reclaimable bytes accumulate between
sweeps — set the interval based on how much disk you’re willing to
lose to a hostile-origin amplification window.
Why iroh-blobs drives the loop instead of us: the sweep
function (gc::gc_run_once) lives in iroh-blobs 0.103’s private
store::gc module and is not re-exported, and Blobs::delete
is pub(crate). The only externally-reachable trigger is
Options::gc. #520 tracks switching to a runtime-driven loop
with manual on-demand GC (admin_v1_cacheGc / decdn node gc)
once upstream exposes the sweep API.
Sourcepub async fn open_full_with_clock(
cache_dir: &Path,
origins: Vec<Arc<dyn Origin>>,
max_blob_mb: u64,
pinned: PinnedHashes,
retry_policy: RetryPolicy,
circuit_breaker: CircuitBreakerPolicy,
metrics: Option<Arc<CacheMetrics>>,
gc_interval: Duration,
clock: Arc<dyn Clock>,
) -> CacheResult<Self>
pub async fn open_full_with_clock( cache_dir: &Path, origins: Vec<Arc<dyn Origin>>, max_blob_mb: u64, pinned: PinnedHashes, retry_policy: RetryPolicy, circuit_breaker: CircuitBreakerPolicy, metrics: Option<Arc<CacheMetrics>>, gc_interval: Duration, clock: Arc<dyn Clock>, ) -> CacheResult<Self>
Self::open_full with an injectable Clock driving the
per-origin circuit-breaker cooldown (#963). Production goes
through open_full (which supplies a SystemClock); tests use
this to drive the breaker’s cooldown deterministically with a
crate::ManualClock.
Sourcepub fn in_flight_total(&self, hash: Hash) -> Option<u64>
pub fn in_flight_total(&self, hash: Hash) -> Option<u64>
Peek the in-flight fill registry for a LIVE fill of hash, returning its
blob total_bytes if one runs. ADVISORY: the serve-miss path uses it to skip
the upstream header handshake when it will coalesce onto a live pull, but the
authoritative own-vs-attach decision stays in Self::claim_fill. See
FillRegistry::in_flight_total.
Sourcepub fn claim_fill(
&self,
hash: Hash,
offset: u64,
len: u64,
total: u64,
make_session: impl FnOnce() -> Arc<FillSession>,
) -> FillClaim
pub fn claim_fill( &self, hash: Hash, offset: u64, len: u64, total: u64, make_session: impl FnOnce() -> Arc<FillSession>, ) -> FillClaim
Atomically claim a serve-miss of [offset, offset+len) (len == 0 = to end)
of the total-byte blob hash: decide attach/own/mixed AND register any new
owner session under one map-lock acquisition (no plan-then-register TOCTOU
race). Returns FillClaim::Attach to coalesce wholly onto a live pull,
FillClaim::Owner with a freshly-registered session covering the whole
request, or FillClaim::Mixed to own a pull for a contiguous remainder while
attaching a sibling for the overlap. make_session builds the session only on
an owning branch (never built-and-dropped on a pure attach). See
FillRegistry::claim.
Sourcepub fn subscribe_inserts(&self) -> Receiver<Hash>
pub fn subscribe_inserts(&self) -> Receiver<Hash>
Subscribe to a stream of Hashes announcing hashes as they become
advertisable in the local store: a whole blob that the private
pull_through committed, or a ranged admit (Self::admit_bao,
Self::admit_bao_stream) that touched a fully present discovery
block, whether the admit succeeded or failed partway. Used by the DHT republish scheduler (ADR
022 §STORE Flow — “When a node verifies its first 64 MiB block of blob
H”) to send the first publish-set to the K+3 closest peers.
One hash can arrive more than once — a ranged fill announces once per block it completes — so a consumer dedupes against its own state.
The channel is bounded and best-effort: lagged receivers see a
broadcast::error::RecvError::Lagged on the next recv and
MUST treat it as “force a full republish sweep” rather than try
to backfill — the cache does not retain the missed hashes.
Sourcepub fn set_pinned(&self, new: &PinnedHashes) -> PinDiff
pub fn set_pinned(&self, new: &PinnedHashes) -> PinDiff
Atomically swap the pinned-hashes set. Called by the runtime’s
SIGHUP handler when cache.pinned_hashes changes — readers (the
eviction-candidate snapshot) observe either the old or the new set,
never a partial mix. Returns a PinDiff so callers can log
“added X, removed Y” without re-walking either set.
Takes &PinnedHashes rather than ownership: callers commonly
want to compute the diff without giving up their own copy. The
leaf PinnedHashes is converted to the engine-internal
HashSet<Hash> (iroh-blobs store hash) here — the cold SIGHUP
path, so the O(n) conversion is acceptable; the hot
is_pinned/eviction_candidates lookups stay on the store hash.
Sourcepub fn pinned_snapshot(&self) -> PinnedHashes
pub fn pinned_snapshot(&self) -> PinnedHashes
Borrow a snapshot of the current pinned set, rebuilt as the leaf
PinnedHashes (config-vocabulary hash) from the engine-internal
store-hash set.
Cost: O(n) — this allocates a fresh HashSet and converts
every hash across the store↔leaf boundary. Call it off the hot
path (it backs SIGHUP reload logging and admin snapshots, not
per-request lookups).
Sourcepub fn retry_policy(&self) -> RetryPolicy
pub fn retry_policy(&self) -> RetryPolicy
Snapshot of the active retry policy. Used by tests and startup logging. The policy is fixed at construction; changes require a restart.
Sourcepub fn is_pinned(&self, hash: Hash) -> bool
pub fn is_pinned(&self, hash: Hash) -> bool
Is hash currently pinned? Cheap O(1) lookup against the live set.
Sourcepub async fn rescan_origins(&self)
pub async fn rescan_origins(&self)
Rebuild the origin-held discovery index (#1130): the hashes this node can serve from a configured origin before any pull-through, so probe and DHT-announce can advertise them. The set is the union of
- every enumerable origin’s
Origin::enumeratelisting (the local filesystem walks its sharded tree; HTTP/S3 enumerate to nothing), and - operator
pinned_hashesthe origin chain actually has (the remote discovery path — HTTP has no listing, S3’s is deliberately not walked, so the pin list is their advertisement set),
each mapped to its total byte size and filtered through Self::refuses
so denied/blacklisted content is never advertised. Rebuilt wholesale and
swapped atomically (like pinned): a concurrent reader sees the old or
the new map, never a partial one.
A candidate whose probe faults — a transport error, or the walk
overrunning the probe timeout — keeps whatever size the previous index
held for it instead of being dropped alongside the genuinely-absent. A
fault is not an authoritative absence, and dropping on one shrinks the
announce set for a whole rescan interval on a throttle window the origin
recovers from in seconds. An origin that faults indefinitely therefore
keeps a carried entry for as long as it keeps faulting; that is the
intended trade, and the serve path answers from the live origin either
way. Only a retry-eligible fault carries forward — a permanent one (a
revoked ACL, a symlink escape) reads the same on every pass, so carrying
it would advertise content this node can never serve until an operator
intervenes. Faults are counted
on decdn_cache_origin_probe_failures_total and reported to the DHT seed
paths through Self::origin_held_snapshot.
An origin whose enumerate fails is the coarser version of the same
thing: it contributes no candidates, so its hashes leave the index with
no per-hash fault and nothing to carry forward. Only operator pins naming
them survive. That leg counts on
decdn_cache_origin_enumerate_failures_total and rides the same
snapshot.
Every adapter reserves Origin::size’s Ok(None) for an
authoritative absence (a genuine 404): an HTTP or S3 5xx/408/429
surfaces as a transient fault and carries forward, a permission decline
as a permanent one — so no outage is dropped here as an absence.
Cost: one origin probe per deduped candidate — every listed entry and
every pin, including the ones that resolve absent and never enter the
index. A local metadata() stat per fs entry, one HTTP HEAD / S3
HeadObject per remote one. Runs off the hot path at the configured
rescan cadence (startup / interval / reload), never per request.
One pass at a time, with any number of triggers arriving during a pass collapsing into a single rerun. A trigger that finds a pass in flight returns immediately rather than awaiting it, so a walk that outruns the cadence cannot accumulate a backlog of waiters — and one rerun covers every trigger it coalesced, because the next pass re-derives everything from scratch.
Sourcepub fn origin_held_snapshot(&self) -> OriginHeldReport
pub fn origin_held_snapshot(&self) -> OriginHeldReport
The hashes in the origin-held index (#1130) together with what the rescan that built it could not resolve, read from one load of the index.
The only way to read the announce set, deliberately: the origin-held half of a DHT seed has no other error channel. A probe fault either carries an older entry forward or leaves the candidate out of the set, a failed enumeration drops a whole origin’s listing, and the seed cannot tell either from an origin that genuinely stopped holding the content. Returning the counts with the hashes — from one load, so a truncated set can never be paired with a later healthy rescan’s counts — is what stops a caller from reporting a short set as a complete one.
hashes is filtered through the live Self::refuses set: the index
is only a per-rescan snapshot, so a hash blacklisted / evicted / denied
after the last rescan is still in it — but must never be announced. The
live filter closes that window without waiting for the next rescan.
Sourcepub fn origin_held_size(&self, hash: Hash) -> Option<u64>
pub fn origin_held_size(&self, hash: Hash) -> Option<u64>
Total byte size of hash if this node can serve it from a configured
origin (#1130), else None. Backs the probe has_blob / total_bytes
answer for origin-held content — Some means “advertise and serve” —
resolved from the last Self::rescan_origins with no per-probe I/O.
Returns None for a Self::refuses-listed hash even if the snapshot
still holds it: the index is rebuilt only on rescan, so a hash
blacklisted / evicted / denied since the last rescan would otherwise be
advertised (probe has_blob: true) for content the serve path would
refuse. The live refusal check is the authority.
Sourcepub async fn origin_probe_size(&self, hash: Hash) -> Option<u64>
pub async fn origin_probe_size(&self, hash: Hash) -> Option<u64>
Total byte size of hash if a configured origin can serve it, resolved
by a live HEAD/HeadObject/stat and memoised (#1130 pt3). This is
the per-probe fallback for the http/s3 discovery gap: Self::rescan_origins
can only index what an origin enumerates, and http/s3 enumerate to
nothing, so a non-pinned bucket object is absent from
Self::origin_held_size. Callers should consult the in-memory index
first (zero I/O) and only fall back here on its miss.
A thin wrapper over Self::origin_probe_presence: Present(size)
advertises Some(size), and both Absent and Fault advertise None
— a probing caller (the probe handler’s has_blob, DHT announce)
never distinguishes “genuinely missing” from “backend unreachable
right now”; either way it must not advertise the blob.
Never fetches the body — existence and size only.
Sourcepub async fn origin_probe_presence(&self, hash: Hash) -> OriginPresence
pub async fn origin_probe_presence(&self, hash: Hash) -> OriginPresence
Live existence probe against the configured origins, distinguishing a
genuine negative from a backend fault (#1766). This is the primitive
behind Self::origin_probe_size; callers that must NOT treat a fault
as an authoritative absence (the origin-only serve gate) use this
directly instead of the size-only wrapper.
A memo hit returns with no I/O. On a miss the origin chain is walked
under a cache.origin_probe_timeout_ms ceiling — deliberately not via
Self::origin_size, whose per-origin error handling is
swallow-and-advance so other callers can fall through a dead origin to a
live one — and this layer decides what to remember:
Present(size)is memoised under the positive TTL;Faultis memoised under the fault TTL (#1789 item 6). The memo is keyed per hash, so this bounds repeat probes OF THE SAME HASH to one liveHEADper fault TTL — the common shape when a client retries a request against a failing origin. It does not bound namespace-wide load: during an outage, N distinct hashes still cost N liveHEADs per window. The TTL is graded against its neighbours and the config resolver enforcesnegative <= fault <= positive: patient enough that a retried hash is not re-probed as often as an absence, eager enough that a recovered origin is noticed soon — a memoised fault costs client-visible availability, since the origin-only serve gate answersInternalErrorand the probe/DHT paths report this node holds nothing for as long as it stands.Absent— every configured origin answeredOk(None), or none is configured at all (a node with nothing configured genuinely holds nothing, which is not transient) — is memoised under the short negative TTL. Every adapter reservesOk(None)for an authoritative absence (a genuine 404) — an outage or a permission decline is anErr, which reaches theFaultarm instead — so this arm never caches a 5xx as an absence.
Never fetches the body — existence and size only.
Sourcepub fn set_origin_probe_config(&self, policy: OriginProbePolicy)
pub fn set_origin_probe_config(&self, policy: OriginProbePolicy)
Install the live-origin-probe configuration at runtime bring-up (#1130
pt3). cache.* is restart-required, so this is called once from the
runtime wiring and swaps the memo wholesale (dropping any warm entries) —
the same “set once, no threading through every test constructor” pattern
as Self::set_max_probe_holds.
Sourcepub fn set_denied(&self, new: &DeniedHashes) -> PinDiff
pub fn set_denied(&self, new: &DeniedHashes) -> PinDiff
Swap in the live local denied set from [content] denied_hashes (ADR
011 § Local Denylist). Returns the delta for the reload log line, same
shape as Self::set_pinned.
Wholesale replacement, not a merge: removing an entry from the denylist
and reloading must un-deny it. Touches only the local slot — a reload
must not drop what the blacklist watcher put in Self::set_chain_denied.
Sourcepub fn is_denied(&self, hash: Hash) -> bool
pub fn is_denied(&self, hash: Hash) -> bool
Is hash on the live denied set? Cheap O(1) lookup.
Prefer Self::refuses unless you specifically need to tell a denial
apart from an eviction — the serve path does, to pick the right refusal
code and metric; nothing else should care.
Sourcepub fn set_chain_denied(&self, hashes: HashSet<Hash>)
pub fn set_chain_denied(&self, hashes: HashSet<Hash>)
Replace the governance deny-set wholesale.
The set records why a hash is refused — governance-sourced, which selects
the wire refusal code — a fact evicted.log (which records only that a
hash was evicted) cannot express. The blacklist watcher rebuilds it each
boot by re-checking every enumerated hash through
isHashBlacklistedForOperator and writing survivors one at a time via
Self::set_chain_denied_one; this bulk setter is retained for tests and
any wholesale reseed.
Sourcepub fn set_chain_denied_one(&self, hash: Hash, denied: bool) -> bool
pub fn set_chain_denied_one(&self, hash: Hash, denied: bool) -> bool
Add or drop one governance-denied hash, returning whether the set actually changed so a caller can skip logging a no-op replay (the watcher re-scans a block range after a restart and re-delivers events it has already applied).
Read-modify-write rather than in-place mutation, because ArcSwap has
no such thing. Blacklist events are governance actions and therefore
rare, so the clone costs nothing next to keeping the read side — which is
on the request hot path — lock-free.
Sourcepub fn is_chain_denied(&self, hash: Hash) -> bool
pub fn is_chain_denied(&self, hash: Hash) -> bool
Is hash on the live governance deny-set? Cheap O(1) lookup.
Prefer Self::refuses unless you specifically need to tell a
governance takedown apart from an eviction or a local denylist entry —
the serve path does, to pick the operator-side metric; nothing else
should care, and the wire code deliberately cannot tell this apart from
Self::is_denied.
Sourcepub fn refuses(&self, hash: Hash) -> bool
pub fn refuses(&self, hash: Hash) -> bool
Does this node refuse to serve, announce, or acquire hash?
The single predicate every “should I expose this blob” decision must
call. is_evicted alone is NOT sufficient and using it directly is the
bug this exists to prevent: a denied-but-still-held hash would keep
being probe-answered has_blob: true and DHT-republished while the
serve path refused it — which under ADR 005 § Probe response is exactly
the signed evidence pair that makes the operator slashable for a
takedown they were discharging.
A quarantined hash is refused too. Its stored bytes failed validation, so a serve or an announce only fails every buyer, and a re-acquisition only lands on the corrupt entry until GC reclaims it.
Sourcepub async fn has(&self, hash: Hash) -> CacheResult<bool>
pub async fn has(&self, hash: Hash) -> CacheResult<bool>
Is this blob already present in the local store?
Returns Ok(false) when the hash has been logically evicted (issue
#279) even if the underlying store still holds the bytes — operators
who call evict expect the node to stop serving immediately, so
has reports the blob as absent.
Sourcepub async fn serve_audit(&self, hash: Hash) -> CacheResult<ServeAudit>
pub async fn serve_audit(&self, hash: Hash) -> CacheResult<ServeAudit>
Serve-path presence + size audit for hash in ONE store actor call.
Answers from a single BlobStatus what the delivery path otherwise asks
in two hops — Self::has for presence, then Self::inspect for size
(#1789 item 7, part B) — saving one store round-trip on every cache hit.
Presence matches Self::has exactly: the blob must be Complete and
no gate may refuse it, so a logically-evicted hash is
ServeAudit::Unavailable even while the store still holds its bytes.
Unlike Self::inspect, a partial blob reports no size here. inspect
exists to tell an operator how much disk a partial pull occupies; this
audit answers what may go on the wire, and a partial blob’s byte count
is not that.
Sourcepub async fn present_ranges(&self, hash: Hash) -> CacheResult<PresentRanges>
pub async fn present_ranges(&self, hash: Hash) -> CacheResult<PresentRanges>
Which chunk ranges of hash are present on disk right now.
Classifies via status() first (so an absent hash returns immediately),
then snapshots the current bitfield by awaiting observe directly — the
FIRST observe item is the current state. It deliberately does NOT call
ObserveProgress::await_completion, which blocks until the blob is
complete and would hang forever on a partial or absent blob.
Logically-evicted hashes report absent, mirroring Self::has. Pure
query: does not touch access times and changes no serving behavior.
Sourcepub async fn observe_present_ranges(
&self,
hash: Hash,
) -> CacheResult<Pin<Box<dyn Stream<Item = ChunkRanges> + Send>>>
pub async fn observe_present_ranges( &self, hash: Hash, ) -> CacheResult<Pin<Box<dyn Stream<Item = ChunkRanges> + Send>>>
A live watch of which chunk ranges of hash are present, for progressive
serve-while-filling (#1621). Yields the current bitfield’s
bao_tree::ChunkRanges first, then further updates as the blob fills.
Mirrors Self::present_ranges’s guards (refuse an evicted/blacklisted
hash, gate a never-seen hash via status() before observing — a hash
with no defined current state has nothing to watch), but stays a live
stream instead of a point-in-time snapshot. Deliberately uses
ObserveProgress::stream(), NEVER await_completion, which blocks
until the blob is complete and would hang forever on a partial blob.
Sourcepub async fn coverage(&self, hash: Hash) -> CacheResult<Coverage>
pub async fn coverage(&self, hash: Hash) -> CacheResult<Coverage>
Which decdn_protocol::DISCOVERY_BLOCK_BYTES discovery blocks of
hash this node can serve from its own cache right now.
Cached blocks only — an origin-backed node that could re-pull the
whole blob on demand does NOT get all-ones here; that capability-aware
widening is the probe handler’s job (it alone knows whether hash has
a configured origin), not this cache-only derivation’s. Absent,
evicted, or otherwise refused hashes report decdn_protocol::Coverage::empty,
mirroring Self::present_ranges’s guards.
A block is covered iff every chunk in its byte span is present: this
reads the blob’s size from the observe() bitfield (via
Self::present_ranges), then diffs each block’s chunk range against
the present ranges the same way Self::missing_ranges diffs a
requested range — a block with any missing chunk is not covered. The
bitfield knows the size as soon as any chunk carries it, so a
front-prefix partial with no validated tail still reports its covered
blocks (where status() would leave the size unknown until the last
chunk). A NotFound, evicted, or refused hash reports no blocks.
Sourcepub async fn coverage_sized(
&self,
hash: Hash,
) -> CacheResult<(Coverage, Option<u64>)>
pub async fn coverage_sized( &self, hash: Hash, ) -> CacheResult<(Coverage, Option<u64>)>
Self::coverage plus the blob size it was derived from, so a partial
holder can advertise its size alongside its blocks. The size is None
exactly when the coverage is empty for want of a known size: an absent,
evicted, or refused hash.
The size comes from the same observe() bitfield read as the coverage,
so the pair costs one read and cannot disagree.
Sourcepub async fn missing_ranges(
&self,
hash: Hash,
byte_offset: u64,
byte_len: u64,
blob_size: u64,
) -> CacheResult<ChunkRanges>
pub async fn missing_ranges( &self, hash: Hash, byte_offset: u64, byte_len: u64, blob_size: u64, ) -> CacheResult<ChunkRanges>
The chunk-aligned sub-ranges of [byte_offset, byte_offset + byte_len)
(byte_len == 0 = to blob_size, an end past blob_size clamped to
it) that are NOT present on disk.
Empty ⇒ the requested span is fully present: a completeness-aware read can
serve it with no fetch, and a resumed pull is a no-op. blob_size is
caller-supplied (the signed total_bytes), the same contract as
Self::export_bao_range_stream.
Sourcepub async fn evict(&self, hash: Hash) -> CacheResult<()>
pub async fn evict(&self, hash: Hash) -> CacheResult<()>
Mark hash as evicted: subsequent Self::has / Self::get calls
behave as if the blob is absent (returning false / NotFound
respectively). The corresponding Self::access_times_snapshot entry
is cleared so future LRU sweeps don’t re-surface the hash.
Serving stops immediately via the logical-evicted set (Blobs::delete
is pub(crate) in iroh-blobs and reserved for the GC task, so the
bytes are not removed synchronously). evict deletes the blob’s
protecting named tag(s) (#860) so the bytes become GC-eligible; the
disk is then reclaimed on the next iroh-blobs GC sweep — cadence
cache.gc_interval_sec, default 5min (#518) — when periodic GC is
enabled. With GC disabled (gc_interval_sec == 0) serving still stops
but the bytes stay on disk until a sweep is configured. Reclaim is
best-effort in the other direction too: the tag deletion is not
allowed to fail the takedown, so if it errors the bytes remain
GC-protected (surfaced via tag_drop_failures) while serving stays
blocked. The operator-visible behavior — the node stops serving the
blob immediately, and reclaims its disk on the next sweep when GC is
enabled and the tag delete succeeded — is what decdn node evict
(issue #279) needs for use cases like DMCA takedown. A stored hash
mismatch that a serve detects does not come here: it takes the
non-durable quarantine, because a corrupt
local copy must stay re-acquirable.
Persisted: the eviction is appended (with fsync) to
<cache_dir>/evicted.log before this call returns successfully, so
the takedown survives a process restart. A persistence failure is
surfaced as Err rather than silently downgrading to in-memory-only —
for DMCA-driven evicts the operator must be able to tell whether
the takedown is durable, otherwise a node restart could resume
serving the content.
Async because the durable append runs the blocking fsync on a
spawn_blocking thread rather than on the async runtime (#845), so a
caller on a request-serving worker does not stall on disk I/O.
Sourcepub async fn inspect(&self, hash: Hash) -> CacheResult<EvictionPreview>
pub async fn inspect(&self, hash: Hash) -> CacheResult<EvictionPreview>
Read-only inspection of a hash’s local cache state, used by
admin_v1_evict { dry_run: true } (issue #379) to preview what
an Self::evict call would touch without mutating any state.
Performs a single BlobStatus query against the underlying
store and reuses the cheap in-memory lookups for pin / access /
already-evicted flags. The returned EvictionPreview also
pre-computes a served bool so admin can derive was_present
without a follow-up Self::has call.
The three in-memory probes are each an O(1) lookup and none of them
blocks: evicted and pinned are ArcSwap loads and access_times is
one DashMap shard. Same pattern as Self::eviction_candidates.
Sourcepub fn is_evicted(&self, hash: Hash) -> bool
pub fn is_evicted(&self, hash: Hash) -> bool
Has this hash been logically evicted via Self::evict?
Lock-free (#1789 item 5): one ArcSwap load and a hash-set probe, so
the serve-path gate takes no mutex and has no lock-poisoning failure
mode that could answer not-evicted for a taken-down hash. The write side
only ever publishes a superset, so an evicted hash is observed evicted
by every reader that linearizes after the swap.
Sourcepub fn is_quarantined(&self, hash: Hash) -> bool
pub fn is_quarantined(&self, hash: Hash) -> bool
Is this hash quarantined because its stored bytes failed validation on a serve export?
A hash mismatch or a short read over held content in
Self::export_bao_range_stream or Self::outboard_pairs quarantines
the hash. Self::refuses then withholds it from serving, announcing,
and re-acquisition. The engine drops its protecting tags, even when the
hash is pinned: the pin does not keep corrupt bytes from GC. The
quarantine lifts on the next Self::has, Self::serve_audit, or
origin rescan that finds the store no longer holds the hash. The quarantine is in memory only.
Sourcepub fn set_max_probe_holds(&self, max: usize)
pub fn set_max_probe_holds(&self, max: usize)
Set the probe-hold budget cap from cache.max_probe_holds (ADR 005
§Hold budget, #318). Called once by the runtime at bring-up. 0
disables the hold path so Self::try_probe_hold returns
ProbeHoldOutcome::HoldsDisabled for any present blob (the node then
answers has_blob: false to every probe).
Sourcepub fn set_origin_read_budget(&self, head_start: Duration, min_bps: u64)
pub fn set_origin_read_budget(&self, head_start: Duration, min_bps: u64)
Set the time budget for each origin read of the range-pull path — one
{H}.obao4 fetch or one data window (ADR 037). Called once by the
runtime at bring-up, from cache.node_pull_stall_window_sec and
cache.node_pull_min_throughput_bps. A read of len bytes gets
head_start + len / min_bps, so the budget scales with the read and
never caps the blob size: the origin must only sustain min_bps on
average after the head start. A read past its budget fails as an origin
transport fault and bumps origin_range_timeouts, so a stuck origin ends
the fill instead of parking it until the client gives up.
A zero head_start or a zero min_bps leaves the reads unbounded. A
whole-body read has no stream to watch for idle gaps, so there is no
idle-only form of the budget. A non-zero head_start below one
millisecond rounds up to one millisecond.
Sourcepub fn origin_read_budget_parts(&self) -> Option<(Duration, u64)>
pub fn origin_read_budget_parts(&self) -> Option<(Duration, u64)>
The origin read budget as (head_start, min_bps), or None when
Self::set_origin_read_budget left the reads unbounded. For the
runtime’s wiring tests.
Sourcepub fn set_frequency_estimator(&self, est: Arc<dyn FrequencyEstimator>)
pub fn set_frequency_estimator(&self, est: Arc<dyn FrequencyEstimator>)
Install the shared frequency estimator. Called once at bring-up when a
tinylfu policy is selected; absent otherwise.
Sourcepub fn has_frequency_estimator(&self) -> bool
pub fn has_frequency_estimator(&self) -> bool
Whether a shared frequency estimator is installed (ADR 040 / ADR 041).
Introspection for bring-up wiring tests: the estimator is shared by any
consumer that needs a heat signal — tinylfu eviction/admission and the
margin serve-economics policy — so this only reports whether some
consumer requested one, not which.
Sourcepub fn set_admission_policy(&self, policy: Arc<dyn AdmissionPolicy>)
pub fn set_admission_policy(&self, policy: Arc<dyn AdmissionPolicy>)
Install the admission policy consulted at store-time (ADR 040). Called
once at bring-up when a non-default policy is selected; otherwise the
engine keeps crate::policy::AlwaysAdmit.
Sourcepub fn set_segment(&self, hash: Hash, segment: Segment)
pub fn set_segment(&self, hash: Hash, segment: Segment)
Set hash’s generic segment membership (ADR 040 §1). Pure in-memory
metadata — the blob’s commit tag still protects it from GC, so this does
no tag I/O. crate::policy::Segment::Main is the absent default, so
setting Main removes any entry; under AlwaysAdmit (always Main)
this is a no-op and the map stays empty.
Sourcepub fn segment_of(&self, hash: Hash) -> Segment
pub fn segment_of(&self, hash: Hash) -> Segment
Read hash’s segment membership; an untracked hash is
crate::policy::Segment::Main.
Sourcepub fn segment_bytes(&self, seg: Segment, sizes: &HashMap<Hash, u64>) -> u64
pub fn segment_bytes(&self, seg: Segment, sizes: &HashMap<Hash, u64>) -> u64
Sum the sizes of the members of seg, using sizes for per-hash bytes.
For Main (the untracked default) this sums every hash in sizes not
present in the segment map.
Sourcepub fn segments_snapshot(&self) -> HashMap<Hash, Segment>
pub fn segments_snapshot(&self) -> HashMap<Hash, Segment>
Snapshot the generic segment membership for the sweep’s
crate::policy::EvictionContext. Only non-default (Probation)
entries are present.
Sourcepub async fn try_probe_hold(&self, hash: Hash) -> CacheResult<ProbeHoldOutcome>
pub async fn try_probe_hold(&self, hash: Hash) -> CacheResult<ProbeHoldOutcome>
Attempt to take (or refresh) a probe-triggered eviction hold on
hash for crate::probe_hold::PROBE_HOLD_DURATION (ADR 005
§Probe-triggered eviction hold).
Returns ProbeHoldOutcome::Held when the blob is present, not
operator-evicted, and a hold was placed for the full window. The other
three variants classify why no hold happened, so the caller need not
re-inspect the cache:
ProbeHoldOutcome::Unavailable— blob absent or operator-evicted.ProbeHoldOutcome::HoldsDisabled— holds disabled by config (max_probe_holds == 0); an intentional operator decision.ProbeHoldOutcome::BudgetExhausted— present but every hold slot is live (max_probe_holds > 0); genuine budget pressure.
Placing a hold and advertising the blob are not the same decision:
BudgetExhausted is advertised despite holding no slot. Callers should
read ProbeHoldOutcome::advertises / ProbeHoldOutcome::hold_placed
rather than comparing against Held.
Per-blob semantics: a hash already held has its expiry refreshed and consumes no additional slot, so many peers probing one popular blob share a single hold (ADR 005 §Hold budget). The common already-held case is an O(1) lookup that skips the O(N) expiry sweep; the sweep runs only when no live hold exists, bounding map growth without a background task.
The operator-evicted set is re-checked while holding the
probe_holds lock, closing the TOCTOU window where a concurrent
Self::evict (DMCA takedown) could land between the initial
Self::has check and granting the hold — a takedown always wins
(ADR 040 §Pinning, durable operator-evict, and the probe-hold stay
engine-enforced).
Sourcepub fn probe_hold_slots_used(&self) -> usize
pub fn probe_hold_slots_used(&self) -> usize
Number of currently-active (non-expired) probe holds, for the
probe_hold_slots_used metric (ADR 005). Sweeps expired entries as
a side effect, so a caller that samples it on its own schedule (the
node’s /metrics scrape) reads live holds even with no probe traffic.
Sourcepub async fn get(&self, hash: Hash) -> CacheResult<Bytes>
pub async fn get(&self, hash: Hash) -> CacheResult<Bytes>
Fetch the blob by hash. Hits the local store on a cache hit; on a miss pulls from the configured origin, BLAKE3-verifies, and inserts before returning.
Error variants callers commonly handle:
CacheError::NoOrigin— miss with no origin configured.CacheError::NotFound— origin returned a definitive not-found (e.g. HTTP 404).CacheError::HashMismatch— origin returned bytes whose BLAKE3 hash didn’t match the request; bytes are dropped, not cached.CacheError::BlobTooLarge/CacheError::OriginError— size cap tripped in the engine or in the origin, respectively.CacheError::Store— local iroh-blobs store I/O failure.
Sourcepub async fn populate(&self, hash: Hash) -> CacheResult<()>
pub async fn populate(&self, hash: Hash) -> CacheResult<()>
Ensure hash is present locally, pulling it through the origin chain on
a miss — like Self::get but WITHOUT returning the bytes or bumping
the bytes_returned / hits counters (which measure bytes served to a
get caller). Use this to fill the cache as a side effect — e.g. the
node-to-node cache-miss pull-through hook (#831) — so an internal fill is
not miscounted as client-facing egress and the whole blob is not
re-assembled into a buffer the caller would just drop. A hit is a no-op.
That second guarantee is enforced, not merely intended: this path is
FillMode::CommitOnly all the way down, and neither commit arm can hand a
payload back under it — the streaming arm skips the read_local, the
buffered arm drops its drain buffer (#1132). Before that the blob was
read back and dropped, which is how the serve path’s miss leg came to hold
~708 MB for a 708 MB blob.
Note the buffered arm’s cost is bounded by
cache.origin_retry.buffered_max_bytes (4 MiB default) rather than by the
mode: an origin that advertises a size_hint at or under it is drained
before commit either way (a None hint always streams, and 0 disables
buffering). CommitOnly guarantees no blob is handed back, not that no
blob is ever briefly buffered.
The origin-egress metric (pull_through_bytes) is still bumped by the
pull, which is correct — those bytes really did leave an origin.
§Errors
Same set as Self::get (NoOrigin / NotFound / HashMismatch /
BlobTooLarge / OriginError / Store).
Sourcepub async fn populate_local(&self, hash: Hash) -> CacheResult<()>
pub async fn populate_local(&self, hash: Hash) -> CacheResult<()>
Like Self::populate, but restricted to the node’s OWN configured
origins (fs/http/s3): the Peer node→node origin is never consulted, so
this fronts no upstream USDC (#1116). The serve path uses it to
reactively fill from a local origin a blob a paying, channel-owning
client asked for — independent of node_to_node_pull_through_enabled —
and to prefer a local origin over the paid peer window path. A hit is a
no-op; absence from every local origin surfaces NotFound, and a chain
with no non-Peer origin surfaces NoOrigin.
§Errors
Same set as Self::populate.
Sourcepub async fn origin_fetch_outboard_bytes(
&self,
hash: Hash,
total_bytes: u64,
) -> CacheResult<Option<Bytes>>
pub async fn origin_fetch_outboard_bytes( &self, hash: Hash, total_bytes: u64, ) -> CacheResult<Option<Bytes>>
The {H}.obao4 outboard for hash (a total_bytes-byte blob): the
cached copy of the exact length, or else the first origin’s copy that
passes the exact-length gate (expected_outboard_len, read with 64
bytes of slack), which is then cached. Ok(None) when no origin serves
one (or none are configured) — the caller degrades exactly as with an
unsupported range. Each origin read runs under
Self::set_origin_read_budget’s budget. A per-origin decline, wrong
length, transport fault or timeout advances the chain.
This is the outboard half of the own-origin serviceability probe
(Self::origin_range_serviceable), which the serve-miss path runs
before it signs a StreamResponse, so a blob no origin can prove is
never advertised as serviceable. A cached copy answers without an origin
read.
The returned outboard is UNTRUSTED until it verifies against the root H
(the range encode in Self::origin_range_wire is where that happens).
§Errors
CacheError::OriginError when no origin serves the outboard and at
least one failed with a transport fault or a timeout (the last one).
Sourcepub async fn origin_range_wire(
&self,
hash: Hash,
aligned: &AlignedRange,
) -> CacheResult<Option<OriginRangeWire>>
pub async fn origin_range_wire( &self, hash: Hash, aligned: &AlignedRange, ) -> CacheResult<Option<OriginRangeWire>>
Stream the header-less interleaved bao wire (ADR 038) for
aligned’s span out of the configured origins, verified against the
root H — the raw-fetch half of a range pull WITHOUT the import (the
node’s NodeAdmitStore sink admits, fed by
decdn_client::BlobSource).
Each origin serves both halves of its own attempt: the outboard and the
data. The origin whose outboard is cached goes first and reuses that copy,
so a draw reads no outboard from the origin while the hash stays cached.
Every other origin, in chain order, reads its own outboard through the
exact-length gate and caches it. The first origin that serves the first
window wins; a per-origin decline or transport fault advances the chain.
Each origin read runs under Self::set_origin_read_budget’s budget. A
verify fault evicts the copy the draw used, so the next draw reads the
outboard again. The wire is produced
window by window (crate::RANGE_PULL_WINDOW_BYTES) by a background
encode that holds a permit from the engine-wide pool of
crate::MAX_CONCURRENT_RANGE_PULLS, so memory stays
O(window + outboard) whatever the span (#2065).
A verify failure here is a HARD fault (CacheError::VerifyFailed), NOT a
degrade: by the time this runs the node has signed a
StreamResponse committing to serve under H, so a corrupt or
misconfigured OWN origin is a local-origin fault to surface, not upstream
corruption to route around (there is no upstream, and no fallback still
honours H). A fault after the wire has started ends it with a terminal
Err from OriginRangeWire::next_chunk.
§Errors
CacheError::VerifyFailed— no origin opened the range, and an origin served a wrong-length outboard or a wrong-length first window; neither can verify againstH.CacheError::OriginError— no origin opened the range, and at least one failed with a transport fault or a timeout (the last such fault).
Ok(None) when every origin cleanly declines.
Sourcepub async fn origin_range_serviceable(
&self,
hash: Hash,
total_bytes: u64,
) -> CacheResult<bool>
pub async fn origin_range_serviceable( &self, hash: Hash, total_bytes: u64, ) -> CacheResult<bool>
The own-origin serviceability probe the serve-miss path runs before it
signs a StreamResponse: whether an origin furnishes both the {H}.obao4
outboard for hash (a total_bytes-byte blob) and ranged reads of its
data. false sends the caller to the buffered whole-blob degrade
(ADR 037 §“Fallback is always correct”), so an origin that publishes an
outboard but declines Range does not have a signed stream fail on its
first draw. The exception is an origin that served ranges earlier and
has stopped since: it fails its draw like any other mid-life origin
fault.
The range half reads the first chunk group through the same origin chain a draw walks, starting at the origin that served the outboard. It reads nothing when that origin has already served a clean range window. The 0-byte blob has no data to range, so its outboard alone answers.
§Errors
CacheError::OriginError— no origin serves both halves, and at least one failed with a transport fault or a timeout.CacheError::VerifyFailed— no origin serves both halves, and an origin served a wrong-length outboard or first window, which cannot verify.
Sourcepub async fn admit_bao(
&self,
hash: Hash,
chunk_ranges: ChunkRanges,
bao_bytes: Bytes,
) -> CacheResult<()>
pub async fn admit_bao( &self, hash: Hash, chunk_ranges: ChunkRanges, bao_bytes: Bytes, ) -> CacheResult<()>
Import an already-encoded interleaved bao range for hash, verified
against the root on import (iroh-blobs import_bao_bytes). Thin
wrapper over the store call, exposed so NodeRangedStore::admit need
not reach into the private store handle. Records recency, like every
fill path, so the partial it leaves is an eviction candidate. An admit
that completes a discovery block announces the hash on
Self::subscribe_inserts.
Sourcepub async fn admit_bao_stream<R>(
&self,
hash: Hash,
chunk_ranges: ChunkRanges,
total_bytes: u64,
reader: R,
session: Option<&Arc<FillSession>>,
) -> Result<R, (R, CacheError)>where
R: AsyncStreamReader + Send,
pub async fn admit_bao_stream<R>(
&self,
hash: Hash,
chunk_ranges: ChunkRanges,
total_bytes: u64,
reader: R,
session: Option<&Arc<FillSession>>,
) -> Result<R, (R, CacheError)>where
R: AsyncStreamReader + Send,
Stream the header-less bao for chunk_ranges of hash (a total_bytes
blob) off reader into the cache as a partial, O(chunk-group),
verifying against the root incrementally. Returns the drained reader
(for BlobSource::finish).
Drives bao_tree‘s ResponseDecoder directly over the header-less wire
(ADR 038 — the size is the trusted total_bytes, not an in-band prefix),
forwarding each decoded BaoContentItem to iroh-blobs’ import_bao
handle. The decoder pulls one chunk at a time and the import channel
backpressures, so memory stays O(one item), not O(range size).
When a serve leg shares this fill (session), the decoder’s Parent items
ARE the outboard proof nodes, so they are captured into the shared session
in the SAME decode pass — no post-admit export_bao read-back that would
re-stream the whole range through the store actor a second time (#1790 item
4). Each leaf’s run of proof nodes goes into the session in one batch just
before that leaf goes to the store, so a parked serve leg encodes its first
frame after the first leaf, not after the whole range, and wakes at most once
per leaf. Front-to-back admits union to the whole tree.
§Errors
CacheError::VerifyFailed— the decoder rejected a chunk group or parent hash against the roothash: the forwarded bytes are corrupt (a lying upstream). Nothing is admitted.CacheError::Feed— a truncated or failed feed offreader, or a zerototal_bytesfor a non-emptyhash: the sender’s fault (distinct from corruption — seeclassify_admit_decode_error).CacheError::Store— a fault of this node’s store or its import channel.
The reader is carried on BOTH result arms: Ok(reader) on success and
Err((reader, err)) on failure. The error arm hands it back so the
caller can recover a typed peer fault the reader parked while filling
(PullStalled/PullTimeout/UpstreamRefused/UpstreamVoucherRejected/
buyer-side LocalPullFault) — that parked fault is the real reason the
stream stopped, and it beats the generic truncated-feed CacheError the
decoder sees. This crate does not read the fault itself (it must not
depend on decdn-client); it only returns the reader so the node ingest
path can.
Sourcepub async fn origin_size(&self, hash: Hash) -> CacheResult<Option<u64>>
pub async fn origin_size(&self, hash: Hash) -> CacheResult<Option<u64>>
Best-effort total byte size of hash from the configured origins, for
scoping a range pull (Self::origin_range_wire needs the exact blob
size to align + verify a sub-range against the root H, and the
{H}.obao4 outboard alone doesn’t pin the final chunk’s length). Walks
the origin fallback chain (Origin::size — HTTP HEAD / S3
HeadObject / fs metadata) and returns the first known size; a
per-origin Ok(None) (no object / compressed / unsupported) or a
transport error advances the chain.
Returns Ok(None) when every origin cleanly declines — the caller MUST
then degrade to a whole-blob Self::populate / Self::get. This is
a metadata probe only: it never fetches or caches bytes, so it carries no
logical-eviction guard (the caller’s serve path and whole-blob fallback
both enforce it).
§Errors
CacheError::NoOriginwhen no origin is configured, so the caller sees a coherent “can’t range-pull” signal rather than a silentNone.CacheError::OriginErrorwhen NO origin answers and at least one failed with a transport fault (the last such fault). A per-origin fault still advances the chain — a later origin’s answer wins — but a probe that ends on faults reports a degraded node, not an empty one, so the serve path’s #1129 latch can turn the terminal miss intoInternalErrorinstead ofNotFound. MirrorsSelf::origin_fetch_outboard_bytes.
Sourcepub async fn shutdown(&self) -> CacheResult<()>
pub async fn shutdown(&self) -> CacheResult<()>
Flush ephemeral state to disk. The iroh-blobs store does its own
cleanup on drop, but only an explicit
iroh_blobs::store::fs::FsStore shutdown guarantees that in-flight
writes survive a crash of the surrounding process, so the runtime
calls this during graceful shutdown.
Sourcepub fn last_accessed(&self, hash: Hash) -> Option<Instant>
pub fn last_accessed(&self, hash: Hash) -> Option<Instant>
Return the last access time for hash, or None if the hash has had
no access since open.
A blob that was on disk at open and has had no access since holds the
open-time recency seed in access_times. The seed is not an access, so
this returns None for it.
Sourcepub fn access_times_snapshot(&self) -> HashMap<Hash, Instant>
pub fn access_times_snapshot(&self) -> HashMap<Hash, Instant>
Collect every recency entry. Eviction logic can sort by value to determine LRU ordering.
Not a point-in-time snapshot: the map is sharded and this walks it shard
by shard, so a record that lands mid-walk may or may not appear (see the
access_times field docs). Recency is advisory input to eviction
ordering, so a hash missed by one walk is seen by the next.
Note: this snapshot is the raw access map. It includes the
open-time recency seed of every blob on disk at open that has had no
access since, and it includes pinned hashes. Eviction implementations
should use Self::eviction_candidates instead, which filters pinned
hashes out so they survive LRU pressure (#276). The raw snapshot is
still exposed because tests and observability paths sometimes want the
unfiltered view.
Sourcepub async fn iter_hashes(&self) -> CacheResult<Vec<Hash>>
pub async fn iter_hashes(&self) -> CacheResult<Vec<Hash>>
Walk every blob in the local iroh-blobs store, complete or partial, and
return its hash, excluding operator-evicted blobs
(ADR 011). Consumed by the DHT
republish scheduler at startup and by its lag sweep (ADR 022 §Bootstrap;
AC 15 cold start, AC 20 lag re-seed): every held blob’s first re-publish time is drawn from
uniform(0, 40 min) per record, so the bootstrap Store rate matches
steady-state by construction. The complementary
Self::subscribe_inserts stream handles fresh commits and completed
discovery blocks during steady state; the two together cover every blob
the node may advertise.
A partial is listed whether or not it covers a whole discovery block
(ADR 022 §STORE Flow: a partial holder publishes once it verifies one).
The walk reads only status(): deciding coverage here would observe()
every partial at boot, and each observe() of an idle partial costs an
fsync on the entry’s idle shutdown. The republisher’s due-time gate reads
coverage instead, one hash at a time across the jittered window, and
drops a partial that covers no block.
This reads on-disk state. Self::open calls it to seed
access_times, so every blob it returns at open is an eviction
candidate. Boot therefore pays this walk twice: once for that seed and
once for the DHT republish seed.
Returns a Vec rather than an async stream because the consumer
drains the input strictly into a hash set, so streaming saves nothing
downstream. The transient is small: at C = 100k blobs the allocation
is roughly 100k × 32 bytes = 3.2 MiB.
Race semantics: a blob evicted between the iroh-blobs list()
emission and the per-hash Self::is_evicted filter would be
included, but downstream publish paths re-check the evicted set, so a
stale entry in the scheduler’s heap fails the membership check at
publish time rather than re-advertising a takedown.
Error semantics: a failure on the iroh-blobs cursor itself
(list() or stream.next()) aborts the walk — we can’t trust
subsequent yields once the cursor is broken. But a per-blob
status() failure logs-and-skips so a single inaccessible blob
doesn’t deny the seed for every other healthy blob. The
republish surface is best-effort by design; partial completion
strictly beats total failure.
Sourcepub fn eviction_candidates(&self) -> EvictionCandidates
pub fn eviction_candidates(&self) -> EvictionCandidates
Return a snapshot of access times excluding pinned hashes. This is the canonical input to LRU eviction (#276): a pinned hash never appears here, so any candidate-picking sort or top-K query run against the result inherently respects the pinning policy.
The exception is a pinned hash that is deny-listed (local or governance deny): it stays a candidate, because deny wins over pin. The deny set itself never removes a hash from the snapshot.
The return type (EvictionCandidates) is a newtype with no
public constructor — callers can iterate or into_inner but
cannot fabricate one. This makes “pinned-already-excluded” a
type-level invariant rather than a documentation claim.
Every blob the open-time store walk finds is a candidate from open. Until it is accessed, it ranks older than every blob accessed since open.
The pinned set is loaded once at the start of the call so a
concurrent set_pinned swap doesn’t change which hashes get
filtered mid-iteration — the snapshot is consistent against
some pinned generation, just not necessarily the very latest.
A third filter layer (after pinned, before the LRU sort) drops any hash under an active probe-triggered eviction hold, even a deny-listed one (#318, ADR 005 §Probe-triggered eviction hold; ADR 040 §Pinning, durable operator-evict, and the probe-hold stay engine-enforced: “a held hash is invisible to the LRU driver until the hold expires”). The held set is swept of expired entries here too, so a node with no probe traffic still releases stale holds.
Sourcepub fn observe_hit(&self, hash: Hash)
pub fn observe_hit(&self, hash: Hash)
Emit the hit signal for hash: bump its LRU recency AND forward one
sighting to the shared frequency estimator (ADR 040 §Hit signal). This is
the one hit sighting a served or get request produces.
Every client-facing serve chokepoint calls this exactly once per served
request — crate::CacheEngine::get on its own path, and the node
crate’s deliver / serve_leg on the paid serve paths. The fill paths
(Self::populate, Self::admit_bao_stream) deliberately do NOT emit
it; they only record recency, so a miss that fills and then serves counts
as ONE sighting (the serve’s), never two. This also
preserves the admission ordering invariant: a fill reads the frequency
estimate for its admission decision before the paired serve emits this
request’s own observe.
Sourcepub fn touch_recency(&self, hash: Hash)
pub fn touch_recency(&self, hash: Hash)
Mark hash as in use now — recency only, no frequency observe. A serve
calls this when it starts, so a blob carrying the open-time recency seed
does not rank oldest, and first in line for eviction, while its bytes are
on the wire. The serve’s one frequency sighting still comes from
Self::observe_hit at clean completion.
Sourcepub async fn size_snapshot(&self) -> CacheResult<HashMap<Hash, u64>>
pub async fn size_snapshot(&self) -> CacheResult<HashMap<Hash, u64>>
Snapshot every on-disk blob keyed by hash with its byte size
(Complete and Partial alike; a partial counts only its present
bytes), the public form of the internal
snapshot_blob_sizes helper. This is the authoritative disk-usage
input for the capacity-eviction driver (#1173): unlike
Self::eviction_candidates — which drops pinned and probe-held hashes
and carries no sizes — this walks the store, so it counts every on-disk
byte, including bytes released but not yet reclaimed by GC.
Cost scales with the total blob set: one status() per blob, plus one
observe() per partial blob whose memoized count is older than 30 s
(each observe() of an idle partial fsyncs its store files). A
partial’s count can therefore trail its disk use by up to 30 s. Call it
on the eviction sweep cadence, not per request.
Sourcepub async fn total_bytes(&self) -> CacheResult<u64>
pub async fn total_bytes(&self) -> CacheResult<u64>
Total on-disk bytes across all blobs, the sum of
Self::size_snapshot. Saturating so an implausibly large store can
never wrap. Drives the eviction driver’s high-water comparison and the
current-size cache-health reporting (#1173).
Sourcepub async fn release_for_eviction(&self, hash: Hash) -> CacheResult<u64>
pub async fn release_for_eviction(&self, hash: Hash) -> CacheResult<u64>
Release hash for capacity eviction: drop its protecting named tag(s)
so the bytes become eligible for the next iroh-blobs GC sweep, and
forget its Self::access_times_snapshot entry so a later LRU sweep
doesn’t re-surface it. Returns the number of tags dropped.
This is the LRU-driver counterpart to Self::evict and is
deliberately not the same path (#1173). evict is the permanent,
fsync’d, evicted.log-backed DMCA takedown: it records the hash in a
durable logical-evicted set that blocks serving forever and survives
restart. Capacity eviction must not do that — a blob dropped only for
space pressure has to be freely re-pullable, and an unbounded takedown
log per LRU eviction would be a durability leak. So this method only
releases GC protection; the blob keeps serving until GC actually
reclaims it, after which Self::has reports it absent naturally
(the bytes are gone from the store, not logically masked).
Pinned hashes are refused (returns Ok(0) without touching state) as a
defense in depth — Self::eviction_candidates already excludes them,
but the caller is a background loop and the pinned set can change
between candidate selection and this call.
The pin exemption is itself carved out for a deny-listed hash
(Self::refuses): “deny wins over pin” on every path, so a pinned +
governance/local-denied hash is still reclaimable here rather than being
held on disk (unservable) until the takedown evict() runs. This only
drops GC protection — the durable takedown record stays evict’s job.
Best-effort against the GC cadence: like evict, actual disk reclaim
only happens when periodic GC is enabled (cache.gc_interval_sec > 0).
Sourcepub async fn export_range(
&self,
hash: Hash,
byte_offset: u64,
byte_len: u64,
) -> CacheResult<Bytes>
pub async fn export_range( &self, hash: Hash, byte_offset: u64, byte_len: u64, ) -> CacheResult<Bytes>
Read [byte_offset, byte_offset + byte_len) of hash from the local
store (ADR 038 §Serve side).
byte_len == 0 reads to the blob end. Works against a partial blob
admitted by Self::admit_bao — only the bytes covered by an
admitted (and thus already-verified) range are readable; asking for
bytes outside the imported span surfaces a CacheError::Store from
iroh-blobs rather than zero-filling.
Concatenates the exported range into a contiguous Bytes; for the
streaming serve path the node drives iroh-blobs’ export_ranges
directly. Provided here so the engine owns the local-read seam for both
whole-blob (Self::get) and range serves.
§Errors
CacheError::Store if the store cannot satisfy the range (blob
absent, the requested bytes were never imported / verified, or a
read-to-end (byte_len == 0) was requested against a partial blob whose
size the store cannot yet report).
Sourcepub async fn export_bao_range(
&self,
hash: Hash,
byte_offset: u64,
byte_len: u64,
blob_size: u64,
) -> CacheResult<Bytes>
pub async fn export_bao_range( &self, hash: Hash, byte_offset: u64, byte_len: u64, blob_size: u64, ) -> CacheResult<Bytes>
Whole-blob-in-memory form of Self::export_bao_range_stream: drains the
stream into one contiguous Bytes. See that method for the wire format and
the range semantics.
Peak memory is the whole aligned range. The paid serve path must NOT use this — it drives the stream directly, so a large blob costs one frame of RAM rather than a copy of the blob (#1132). Every caller today is a test that needs a buffer to assert against; it is kept for them, and because the ADR 038 round-trip tests are more legible over a buffer than a stream.
§Errors
Everything Self::export_bao_range_stream can fail with, except that a
fault discovered while exporting — including the truncation refusal —
surfaces here as an Err return rather than as a terminal stream item,
because nothing has been handed to a consumer yet.
Sourcepub async fn export_bao_range_stream(
&self,
hash: Hash,
byte_offset: u64,
byte_len: u64,
blob_size: u64,
) -> CacheResult<Pin<Box<dyn Stream<Item = CacheResult<Bytes>> + Send>>>
pub async fn export_bao_range_stream( &self, hash: Hash, byte_offset: u64, byte_len: u64, blob_size: u64, ) -> CacheResult<Pin<Box<dyn Stream<Item = CacheResult<Bytes>> + Send>>>
Export [byte_offset, byte_offset + byte_len) of hash as the
header-less bao interleaved verified-stream encoding that travels on
cdn/client/v1 (ADR 038 §Serve side),
yielded incrementally as the store produces it. byte_len == 0 exports to
the blob end. This is the only client-facing delivery path — there is no
raw-byte fallback (ADR 038 AC#4) — so a whole-blob serve passes
byte_offset == 0, byte_len == 0.
The bytes are proof nodes (64 B each) interleaved with chunk-group data in
tree order, without the 8-byte size header: the signed
StreamResponse.body.total_bytes is the authoritative size, so the
receiver builds its own BaoTree and never reads a header off the wire
(avoids a second, unauthenticated size source — ADR 038 §Wire format).
The range widens to enclosing 16 KiB chunk-group boundaries
(align_range_clamped) because a bao proof anchors whole groups; an
end past the blob clamps to it rather than being refused: a claimed
size is a hint. The serve side does not trim back to the requested offset
(trimming would break verification). The receiver discards the
group-aligned prefix. The outboard is read from the store (built at
import).
Works against a partial blob (only imported/verified chunk groups are
exportable, exactly like Self::export_range).
Each item is one export item’s serialization — a 64-byte proof pair or one chunk group’s data — so a consumer that writes items straight to the wire holds O(chunk group) rather than O(blob). This is what the paid serve path drives (#1132), so a 708 MB blob costs O(chunk group) resident per concurrent serve, not ~708 MB.
§Held content is validated as it is exported
The store validates each exported item against the content root before
it yields the item: every parent pair against the hash its parent
expects, and every leaf’s data against its parent’s hash. Bytes or an
outboard that diverged on disk after admission therefore never reach
the stream. The export ends at the first mismatching chunk group with a
terminal Err item, and the engine
quarantines the hash.
§Truncation is reported mid-stream
The !done refusal below (the store’s item channel closing without a
terminal Done/Error — an actor crash or shutdown race) can only be
detected at the END of the stream, by which point a streaming consumer has
already put earlier bytes on the wire. It therefore surfaces as a terminal
Err item rather than as a pre-flight error, and the consumer must
abort the delivery on it. The billing invariant is unchanged: the client
sees a short delivery, rejects it, and never pays the closing voucher — but
the detection point is after the last byte, not before the first
(#915 review, #1132).
§Errors
CacheError::Store if byte_offset is at or past the blob end, or
(for the 0-byte case) the blob is absent. An end past the blob clamps.
Faults discovered while exporting, the truncation refusal included,
arrive as Err items in the stream.
Sourcepub async fn outboard_pairs(
&self,
hash: Hash,
chunk_ranges: &ChunkRanges,
) -> CacheResult<Vec<(TreeNode, (Hash, Hash))>>
pub async fn outboard_pairs( &self, hash: Hash, chunk_ranges: &ChunkRanges, ) -> CacheResult<Vec<(TreeNode, (Hash, Hash))>>
Collect the outboard (node, (left, right)) hash pairs iroh-blobs emits for
chunk_ranges of hash, read from the store’s outboard.
The store validates each pair and each leaf of the range against the content
root as it exports them, like Self::export_bao_range_stream. A stored
hash mismatch returns Err and quarantines the
hash.
The serve leg’s shared outboard (ADR 038) is fed from these so it can drive
a coherent whole-range bao encode while the pull fills the cache
incrementally. Call it for each range as it is admitted (and for the held
part of the serve’s range at serve start). It reads and verifies every leaf
of chunk_ranges, so its cost grows with the held bytes it is given.
export_bao emits every proof Parent on the path to the range PLUS the
right-siblings covering
still-absent content, so the union over a front-to-back admit sequence is the
whole tree’s internal nodes. Leaf data and the size header are skipped.
chunk_ranges must be PRESENT (a just-admitted or held range): the proof
nodes for absent siblings are emitted from the outboard regardless, but a
range whose own leaves are absent faults export_bao. On a partial blob
that fault can be a hash mismatch, because an absent range reads back as
zeros. The quarantine ignores a mismatch outside the present ranges, so
a precondition breach returns Err without withdrawing the hash.
Trait Implementations§
Source§impl Clone for CacheEngine
impl Clone for CacheEngine
Auto Trait Implementations§
impl !RefUnwindSafe for CacheEngine
impl !UnwindSafe for CacheEngine
impl Freeze for CacheEngine
impl Send for CacheEngine
impl Sync for CacheEngine
impl Unpin for CacheEngine
impl UnsafeUnpin for CacheEngine
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more