Skip to main content

CacheEngine

Struct CacheEngine 

Source
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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

pub fn is_pinned(&self, hash: Hash) -> bool

Is hash currently pinned? Cheap O(1) lookup against the live set.

Source

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::enumerate listing (the local filesystem walks its sharded tree; HTTP/S3 enumerate to nothing), and
  • operator pinned_hashes the 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.

Source

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.

Source

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.

Source

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.

Source

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;
  • Fault is 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 live HEAD per 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 live HEADs per window. The TTL is graded against its neighbours and the config resolver enforces negative <= 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 answers InternalError and the probe/DHT paths report this node holds nothing for as long as it stands.
  • Absent — every configured origin answered Ok(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 reserves Ok(None) for an authoritative absence (a genuine 404) — an outage or a permission decline is an Err, which reaches the Fault arm instead — so this arm never caches a 5xx as an absence.

Never fetches the body — existence and size only.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn segment_of(&self, hash: Hash) -> Segment

Read hash’s segment membership; an untracked hash is crate::policy::Segment::Main.

Source

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.

Source

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.

Source

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:

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).

Source

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.

Source

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:

Source

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).

Source

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.

Source

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).

Source

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 against H.
  • 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.

Source

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.
Source

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.

Source

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)>

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 root hash: the forwarded bytes are corrupt (a lying upstream). Nothing is admitted.
  • CacheError::Feed — a truncated or failed feed off reader, or a zero total_bytes for a non-empty hash: the sender’s fault (distinct from corruption — see classify_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.

Source

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::NoOrigin when no origin is configured, so the caller sees a coherent “can’t range-pull” signal rather than a silent None.
  • CacheError::OriginError when 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 into InternalError instead of NotFound. Mirrors Self::origin_fetch_outboard_bytes.
Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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).

Source

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).

Source

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.

Source

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.

Source

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

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for CacheEngine

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<Unshared, Shared> IntoShared<Shared> for Unshared
where Shared: FromUnshared<Unshared>,

Source§

fn into_shared(self) -> Shared

Creates a shared type from an unshared type.
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more