Skip to main content

decdn_cache/
fill_session.rs

1//! `FillSession` — the cache-resident fill-coordination primitive the
2//! serve-miss legs share (ADR 038).
3//!
4//! # Why this exists
5//!
6//! The serve leg must emit ONE coherent whole-range bao verified stream
7//! while the pull leg fills the cache incrementally beside it. `bao_tree`'s async
8//! [`encode_ranges_validated`](bao_tree::io::fsm::encode_ranges_validated) is the
9//! right engine — it walks the range pre-order, reading leaf DATA and, for every
10//! internal node, the `(left, right)` hash pair from an [`Outboard`]. The data the
11//! serve reads from the cache (via ranged export, gated on the present-range watch).
12//! But the OUTBOARD it cannot: iroh-blobs keeps a partial blob's outboard in actor
13//! memory and exposes no reader, and a pre-order walk needs proof hashes for
14//! not-yet-filled right-spine subtrees (the root's `right_hash` is emitted before
15//! any right-half data lands) that are NOT derivable from present data.
16//!
17//! So the cache keeps its own copy, captured as each range admits (the pull leg
18//! already receives those exact proof nodes in the verified upstream bao it ingests,
19//! and the cache's `admit_bao_stream` captures them via [`FillSession::capture`] into
20//! a whole-tree buffer). The serve leg reads them through a [`SessionOutboardReader`],
21//! whose [`Outboard::load`] AWAITS a node that has not been captured yet — racing the
22//! registry-wide fill liveness so a fill that dies (or a genuinely missing node after
23//! a clean pull) fails the serve rather than hanging.
24//!
25//! # The outboard is per-HASH, shared by every fill of that hash
26//!
27//! A blob is BLAKE3-addressed, so its tree geometry — and thus every captured
28//! `(left, right)` node — is a property of the HASH, not of any single pull. When two
29//! serve-misses want overlapping bytes of one hash, one opens a pull for its
30//! remainder and the other for its own; both admit into the SAME store and each admit
31//! captures proof nodes into the ONE per-hash [`HashOutboard`]. Because
32//! `attach ∪ remainder = R` by construction, the two pulls together capture every
33//! proof node a coherent encode of `R` needs, into that single buffer. A serve leg
34//! reading `R` therefore does not care which pull filled what — it reads the shared
35//! per-hash outboard and the source-agnostic present-range watch.
36//!
37//! The one thing that must generalize past a single pull is termination: "await this
38//! node, or fail" has to consult ALL live fills whose `covered` ranges intersect the
39//! node's byte span, not one pull's terminal signal. [`FillSession::range_still_live`]
40//! (backed by [`FillRegistry::range_still_live`]) answers exactly that — it settles
41//! a parked reader once no live session's `covered` still intersects the range,
42//! which is the N-fill generalization of the single-pull failure race. A settled
43//! read is served from the local store when the store holds what it needs, and
44//! fails otherwise; it never hangs.
45//!
46//! # Ownership seam (cache owns the structure, the node drives it)
47//!
48//! The pull leg (paid peer or own-origin) is node-side and alone knows when/why it
49//! terminated. So the session exposes [`FillSession::mark_ended`]; the node's pull
50//! leg calls it on every exit. The cache owns the DATA STRUCTURE; the node DRIVES
51//! it. [`FillError`] is a flattened terminal message (`anyhow::Error` is not `Clone`).
52
53use std::collections::HashMap;
54use std::io;
55use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
56use std::sync::{Arc, Mutex as StdMutex, OnceLock, PoisonError, Weak};
57
58use bao_tree::io::fsm::Outboard;
59use bao_tree::{BaoTree, BlockSize, ChunkNum, ChunkRanges, TreeNode, blake3};
60use decdn_bao_range::align_range_clamped;
61use tokio::sync::Notify;
62use tokio_util::sync::CancellationToken;
63
64use crate::Hash;
65use crate::engine::CacheEngine;
66use crate::error::{CacheResult, ErrorChain};
67
68/// iroh-blobs' block size — 16 KiB chunk groups (`2^4` 1 KiB chunks). Identical by
69/// construction to `decdn_bao_range::IROH_BLOCK_SIZE` and
70/// `iroh_blobs::store::IROH_BLOCK_SIZE`; the tree geometry (and thus every node
71/// offset) must match the store's, since the captured hashes are the store's.
72const IROH_BLOCK_SIZE: BlockSize = BlockSize::from_chunk_log(4);
73
74/// Bytes per outboard node (a `(left, right)` blake3 hash pair).
75const HASH_PAIR_BYTES: usize = 64;
76
77/// Bytes per bao chunk (the `ChunkNum` unit). `ChunkNum(n)` starts at `n * 1024`.
78const CHUNK_BYTES: u64 = 1024;
79
80/// A flattened terminal error for a fill: the node's pull leg records why its pull
81/// ended, and the serve-side [`SessionOutboardReader`] / data reader surface it.
82/// A `String` message rather than `anyhow::Error` because the outcome must be
83/// `Clone` (every observer reads it) and `anyhow::Error` is not.
84#[derive(Debug, Clone)]
85pub struct FillError {
86    msg: String,
87    held_bytes_cap: bool,
88}
89
90impl FillError {
91    /// Wrap a flattened terminal message.
92    #[must_use]
93    pub fn new(msg: impl Into<String>) -> Self {
94        Self {
95            msg: msg.into(),
96            held_bytes_cap: false,
97        }
98    }
99
100    /// A fill that ended because storing more of its range would push the
101    /// bytes this node holds for the blob past its `max_blob_size` cap. The
102    /// node serves what it holds and refuses the rest, so a serve that reads
103    /// past it ends as a refusal, not as a node fault.
104    #[must_use]
105    pub fn held_bytes_cap(msg: impl Into<String>) -> Self {
106        Self {
107            msg: msg.into(),
108            held_bytes_cap: true,
109        }
110    }
111
112    /// Whether the fill ended at the cap on bytes held ([`Self::held_bytes_cap`]).
113    #[must_use]
114    pub const fn is_held_bytes_cap(&self) -> bool {
115        self.held_bytes_cap
116    }
117
118    /// The I/O error a serve read raises for bytes this ended fill did not
119    /// land, prefixed by `context`. The fill's error stays in the error's
120    /// source chain, so [`ended_at_held_bytes_cap`] finds it, and
121    /// [`is_unlanded`] holds for it.
122    #[must_use]
123    pub fn unlanded(&self, context: impl Into<String>) -> io::Error {
124        io::Error::other(Unlanded {
125            context: context.into(),
126            cause: Some(self.clone()),
127        })
128    }
129}
130
131/// The I/O error a serve read raises for bytes no fill landed and the store
132/// does not hold, when no ended fill of its own names the cause: the covering
133/// fills were siblings. [`is_unlanded`] holds for it.
134#[must_use]
135pub fn unlanded(context: impl Into<String>) -> io::Error {
136    io::Error::other(Unlanded {
137        context: context.into(),
138        cause: None,
139    })
140}
141
142impl std::fmt::Display for FillError {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.write_str(&self.msg)
145    }
146}
147
148impl std::error::Error for FillError {}
149
150/// A serve read of bytes that no fill landed and the store does not hold
151/// ([`FillError::unlanded`], [`unlanded`]), with the ended fill's error when
152/// this serve's own fill ended.
153#[derive(Debug)]
154struct Unlanded {
155    context: String,
156    cause: Option<FillError>,
157}
158
159impl std::fmt::Display for Unlanded {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        match &self.cause {
162            Some(cause) => write!(f, "{}: {cause}", self.context),
163            None => f.write_str(&self.context),
164        }
165    }
166}
167
168impl std::error::Error for Unlanded {
169    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
170        self.cause
171            .as_ref()
172            .map(|cause| cause as &(dyn std::error::Error + 'static))
173    }
174}
175
176/// Whether `err` or an error in its source chain, including the payload of an
177/// [`io::Error`], is a fill that ended at the cap on bytes held
178/// ([`FillError::held_bytes_cap`]).
179#[must_use]
180pub fn ended_at_held_bytes_cap(err: &(dyn std::error::Error + 'static)) -> bool {
181    chain_has(err, &|e| {
182        e.downcast_ref::<FillError>()
183            .is_some_and(FillError::is_held_bytes_cap)
184    })
185}
186
187/// Whether `err` or an error in its source chain, including the payload of an
188/// [`io::Error`], is a serve read of bytes that no fill landed and the store
189/// does not hold ([`FillError::unlanded`], [`unlanded`]). A store that failed
190/// to read bytes it holds raises no such error.
191#[must_use]
192pub fn is_unlanded(err: &(dyn std::error::Error + 'static)) -> bool {
193    chain_has(err, &|e| e.downcast_ref::<Unlanded>().is_some())
194}
195
196/// Whether `is` holds for `err` or for an error in its source chain,
197/// descending into the payload of each [`io::Error`].
198fn chain_has(
199    err: &(dyn std::error::Error + 'static),
200    is: &dyn Fn(&(dyn std::error::Error + 'static)) -> bool,
201) -> bool {
202    let mut next = Some(err);
203    while let Some(e) = next {
204        if is(e) {
205            return true;
206        }
207        if let Some(payload) = e.downcast_ref::<io::Error>().and_then(io::Error::get_ref)
208            && chain_has(payload, is)
209        {
210            return true;
211        }
212        next = e.source();
213    }
214    false
215}
216
217/// The pre-order outboard buffer + a per-node "captured" flag. Guarded together;
218/// the lock is never held across an await.
219///
220/// Both vectors are allocated lazily on the first capture (see
221/// [`HashOutboard::write_pair`]), not at construction: empty ⇒ nothing captured
222/// yet, which [`HashOutboard::try_load`] reads as "absent" exactly as an all-false
223/// `captured` did. This keeps the MB-scale zeroing for a large blob off the global
224/// registry lock that [`HashOutboard::new`] runs under.
225#[derive(Debug)]
226struct OutboardState {
227    /// `tree.outboard_size()` bytes once sized: node `n`'s pair lives at
228    /// `tree.pre_order_offset(n) * 64`. Empty until the first capture sizes it.
229    bytes: Vec<u8>,
230    /// `captured[i]` is set once node index `i` (its `pre_order_offset`) is saved.
231    /// Empty until the first capture sizes it, alongside `bytes`.
232    captured: Vec<bool>,
233}
234
235/// The captured whole-tree outboard for ONE hash, shared by every [`FillSession`]
236/// that fills that hash. A blob's tree geometry is fixed by its BLAKE3 root, so a
237/// node captured by any pull — of any range, from any source — is valid for every
238/// serve leg reading that hash. The registry keys exactly one of these per hash and
239/// every session for the hash references it, which is what makes partial-overlap
240/// proof-sourcing free: pull A (admitting `attach`) and pull B (admitting
241/// `remainder`) capture into the same buffer, and a serve leg for `R = attach ∪
242/// remainder` reads every node either supplied.
243#[derive(Debug)]
244pub struct HashOutboard {
245    tree: BaoTree,
246    root: blake3::Hash,
247    state: StdMutex<OutboardState>,
248    /// Notified after every [`Self::capture`] so a parked [`Outboard::load`]
249    /// re-checks. Runtime-agnostic, so it crosses the pull/serve runtimes safely.
250    captured: Notify,
251    /// Notified when ANY session for this hash ends or is cancelled, so a parked
252    /// reader re-checks [`FillRegistry::range_still_live`] and fails a range no live
253    /// pull will fill. Shared as an `Arc` so a data reader on the node side can hold
254    /// an owned handle without naming this cache-private type.
255    liveness: Arc<Notify>,
256}
257
258impl HashOutboard {
259    /// Build the empty outboard for a `total_bytes`-byte blob rooted at `root`.
260    ///
261    /// O(1): the `tree.outboard_size()`-byte buffer is NOT allocated here — the
262    /// first [`Self::write_pair`] sizes it under the per-hash `state` lock. This
263    /// keeps a session's outboard construction under the global [`FillRegistry`]
264    /// lock free of MB-scale zeroing, so one large-blob claim does not stall
265    /// every other hash's claim/wakeup for the allocation duration, and a session
266    /// that only adopts the canonical outboard allocates nothing to discard.
267    #[must_use]
268    fn new(root: blake3::Hash, total_bytes: u64) -> Arc<Self> {
269        Arc::new(Self {
270            tree: BaoTree::new(total_bytes, IROH_BLOCK_SIZE),
271            root,
272            state: StdMutex::new(OutboardState {
273                bytes: Vec::new(),
274                captured: Vec::new(),
275            }),
276            captured: Notify::new(),
277            liveness: Arc::new(Notify::new()),
278        })
279    }
280
281    /// Write one internal node's `(left, right)` hash pair into `state`, returning
282    /// whether the pair was actually persisted. `false` for a leaf (no outboard
283    /// slot), an offset past `usize`, or a slot outside the buffers — nothing was
284    /// stored, so the caller must NOT wake readers on its behalf. `true` only once
285    /// both the hash bytes and the captured flag are set, which is exactly the state
286    /// [`Self::try_load`] reads back. Idempotent: re-saving a node (a re-admitted
287    /// range) overwrites with the same bytes. Does NOT notify; the caller wakes
288    /// parked readers once the batch is in, so an N-node admit fires `captured` a
289    /// single time rather than N.
290    fn write_pair(
291        &self,
292        state: &mut OutboardState,
293        node: TreeNode,
294        pair: (blake3::Hash, blake3::Hash),
295    ) -> bool {
296        let Some(offset) = self.tree.pre_order_offset(node) else {
297            return false; // leaf: no hash pair in the outboard
298        };
299        let Ok(idx) = usize::try_from(offset) else {
300            return false; // offset beyond usize: no addressable slot
301        };
302        // Lazily size the outboard on the first capture, under this per-hash
303        // `state` lock rather than the global registry lock `new` runs under. A
304        // real interior node (a `Some` offset) implies `outboard_size() >= 64`, so
305        // an empty buffer here always means "not yet sized", never a zero-node
306        // tree. Sized exactly once: every later capture sees a non-empty buffer.
307        // Bail if the size is not `usize`-representable rather than allocating
308        // `usize::MAX` — the same "not addressable, not capturable" degrade as the
309        // `idx` guard above, never a process-aborting allocation.
310        if state.bytes.is_empty() {
311            let Ok(size) = usize::try_from(self.tree.outboard_size()) else {
312                return false;
313            };
314            state.bytes = vec![0u8; size];
315            state.captured = vec![false; size / HASH_PAIR_BYTES];
316        }
317        let byte_off = idx.saturating_mul(HASH_PAIR_BYTES);
318        let (l, r) = pair;
319        let bytes_written = if let Some(slot) =
320            state.bytes.get_mut(byte_off..byte_off + HASH_PAIR_BYTES)
321            && let Some((left, right)) = slot.split_at_mut_checked(32)
322        {
323            left.copy_from_slice(l.as_bytes());
324            right.copy_from_slice(r.as_bytes());
325            true
326        } else {
327            false
328        };
329        let flag_set = if let Some(flag) = state.captured.get_mut(idx) {
330            *flag = true;
331            true
332        } else {
333            false
334        };
335        bytes_written && flag_set
336    }
337
338    /// Capture one internal node's `(left, right)` hash pair, then wake parked
339    /// readers. A `node` with no outboard slot (a leaf) is ignored and does not notify.
340    fn capture(&self, node: TreeNode, pair: (blake3::Hash, blake3::Hash)) {
341        let wrote = {
342            let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
343            self.write_pair(&mut state, node, pair)
344        };
345        if wrote {
346            self.captured.notify_waiters();
347        }
348    }
349
350    /// Capture a batch of node pairs under one lock acquisition, then fire
351    /// `captured` exactly once. A serve leg parked on any of these nodes reads them
352    /// all back the same as a per-node [`Self::capture`] loop would, but every parked
353    /// reader wakes once per batch instead of once per node. The admit path hands
354    /// each leaf's run of proof nodes here just before the leaf. Leaves are ignored;
355    /// the notify fires only if at least one internal node was written.
356    fn capture_many(
357        &self,
358        pairs: impl IntoIterator<Item = (TreeNode, (blake3::Hash, blake3::Hash))>,
359    ) {
360        let wrote = {
361            let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
362            pairs.into_iter().fold(false, |acc, (node, pair)| {
363                self.write_pair(&mut state, node, pair) || acc
364            })
365        };
366        if wrote {
367            self.captured.notify_waiters();
368        }
369    }
370
371    /// Read node `idx`'s captured pair, or `None` if not captured yet. Never holds
372    /// the lock across an await.
373    fn try_load(&self, idx: usize) -> Option<(blake3::Hash, blake3::Hash)> {
374        let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
375        if state.captured.get(idx).copied() != Some(true) {
376            return None;
377        }
378        let byte_off = idx.saturating_mul(HASH_PAIR_BYTES);
379        let slot = state.bytes.get(byte_off..byte_off + HASH_PAIR_BYTES)?;
380        let (l, r) = slot.split_at_checked(32)?;
381        let left: [u8; 32] = l.try_into().ok()?;
382        let right: [u8; 32] = r.try_into().ok()?;
383        Some((blake3::Hash::from(left), blake3::Hash::from(right)))
384    }
385}
386
387/// A forward-only content frontier shared between a fill's serve legs and its pull.
388///
389/// Holders read it with [`Self::get`]. Only the owning [`FillSession`] raises it,
390/// and every raise that moves the value wakes a pull parked in
391/// [`DownstreamWatch::past`]. No holder can lower the frontier or raise it without
392/// that wakeup, so a parked pull never misses an advance.
393#[derive(Debug, Clone)]
394pub struct Frontier {
395    /// The frontier value, a high-water mark.
396    value: Arc<AtomicU64>,
397    /// The session's downstream wakeup, notified when [`Self::raise`] moves `value`.
398    advanced: Arc<Notify>,
399}
400
401impl Frontier {
402    /// A frontier at `start` that notifies `advanced` on each advance.
403    fn new(start: u64, advanced: Arc<Notify>) -> Self {
404        Self {
405            value: Arc::new(AtomicU64::new(start)),
406            advanced,
407        }
408    }
409
410    /// The current frontier value.
411    #[must_use]
412    pub fn get(&self) -> u64 {
413        self.value.load(Ordering::Acquire)
414    }
415
416    /// Raise the frontier to `to` and wake the parked pull if the value moved. A
417    /// lower `to` changes nothing.
418    fn raise(&self, to: u64) {
419        if self.value.fetch_max(to, Ordering::AcqRel) < to {
420            self.advanced.notify_waiters();
421        }
422    }
423}
424
425/// A fill's serve demand: one standing demand per serve leg that has run out of
426/// encoded bytes while its encode waits on content this fill produces.
427///
428/// A leg stands its demand through its [`DemandSlot`] and withdraws it once its
429/// encode moves again or the leg ends. [`Self::get`] reads the NEAREST standing
430/// demand, so a leg parked far down the blob never hides a leg parked at the pull's
431/// frontier. Every change to the nearest demand wakes a pull parked in
432/// [`DownstreamWatch::past`].
433#[derive(Debug, Clone)]
434pub struct Demand {
435    /// The content end each starved serve leg awaits, keyed by its slot id.
436    legs: Arc<StdMutex<HashMap<u64, u64>>>,
437    /// The session's downstream wakeup, shared with its paid frontier.
438    advanced: Arc<Notify>,
439}
440
441impl Demand {
442    /// An empty demand that notifies `advanced` when its nearest value changes.
443    fn new(advanced: Arc<Notify>) -> Self {
444        Self {
445            legs: Arc::new(StdMutex::new(HashMap::new())),
446            advanced,
447        }
448    }
449
450    /// The nearest content end a starved serve leg awaits, or `0` when none waits.
451    #[must_use]
452    pub fn get(&self) -> u64 {
453        nearest(&self.legs.lock().unwrap_or_else(PoisonError::into_inner))
454    }
455
456    /// Record that leg `leg` awaits content up to `end`.
457    fn stand(&self, leg: u64, end: u64) {
458        self.update(|legs| {
459            legs.insert(leg, end);
460        });
461    }
462
463    /// Drop leg `leg`'s standing demand, if any.
464    fn withdraw(&self, leg: u64) {
465        self.update(|legs| {
466            legs.remove(&leg);
467        });
468    }
469
470    /// Apply `change` and wake the parked pull when the nearest demand moved.
471    fn update(&self, change: impl FnOnce(&mut HashMap<u64, u64>)) {
472        let moved = {
473            let mut legs = self.legs.lock().unwrap_or_else(PoisonError::into_inner);
474            let before = nearest(&legs);
475            change(&mut legs);
476            nearest(&legs) != before
477        };
478        if moved {
479            self.advanced.notify_waiters();
480        }
481    }
482}
483
484/// The smallest standing demand in `legs`, or `0` when it is empty.
485fn nearest(legs: &HashMap<u64, u64>) -> u64 {
486    legs.values().copied().min().unwrap_or(0)
487}
488
489/// Source of [`DemandSlot`] ids, unique across every fill in the process.
490static NEXT_DEMAND_SLOT: AtomicU64 = AtomicU64::new(1);
491
492/// One serve leg's handle on the serve demand of its blob's live fills. Minted by
493/// [`FillSession::demand_slot`].
494///
495/// [`Self::stand`] routes the leg's demand to the live fill whose covered range
496/// holds the awaited byte, and withdraws it from every other fill of the hash:
497/// only that fill's pull can produce the byte. Dropping the slot withdraws the
498/// demand, so a finished or abandoned leg leaves nothing standing.
499#[derive(Debug)]
500pub struct DemandSlot {
501    /// The serve leg's own session, the route to the hash's registry.
502    session: Arc<FillSession>,
503    /// This slot's key in every [`Demand`] it stands in.
504    id: u64,
505}
506
507impl DemandSlot {
508    /// Stand this leg's demand for content up to `end`, clamped to the blob size.
509    /// It replaces the leg's previous demand. An `end` of `0` withdraws it.
510    pub fn stand(&self, end: u64) {
511        let end = end.min(self.session.total_bytes());
512        let Some(byte) = end.checked_sub(1) else {
513            self.withdraw();
514            return;
515        };
516        match self.session.registry_and_hash() {
517            Some((registry, hash)) => registry.route_demand(hash, self.id, byte, end),
518            None => self.session.route_own_demand(self.id, byte, end),
519        }
520    }
521
522    /// Withdraw this leg's demand from every fill of the hash.
523    pub fn withdraw(&self) {
524        match self.session.registry_and_hash() {
525            Some((registry, hash)) => registry.withdraw_demand(hash, self.id),
526            None => self.session.serve_demand.withdraw(self.id),
527        }
528    }
529}
530
531impl Drop for DemandSlot {
532    fn drop(&mut self) {
533        self.withdraw();
534    }
535}
536
537/// A pull leg's view of its session's two downstream frontiers: the paid frontier
538/// ([`FillSession::served_frontier`]) and the serve demand
539/// ([`FillSession::serve_demand`]). Read-only and cheap to clone, so a pull on its
540/// own runtime holds an owned copy. Minted by [`FillSession::downstream_watch`].
541#[derive(Debug, Clone)]
542pub struct DownstreamWatch {
543    /// The session's paid content frontier.
544    served_paid: Frontier,
545    /// The session's serve demand. Shares its wakeup with `served_paid`.
546    serve_demand: Demand,
547}
548
549impl DownstreamWatch {
550    /// The current paid content frontier.
551    #[must_use]
552    pub fn served_paid(&self) -> u64 {
553        self.served_paid.get()
554    }
555
556    /// The nearest content end a starved serve leg awaits, or `0` when none waits.
557    #[must_use]
558    pub fn serve_demand(&self) -> u64 {
559        self.serve_demand.get()
560    }
561
562    /// Resolve once `served_paid` lies past `observed_paid` or the serve demand
563    /// differs from `observed_demand`. Returns at once when either already holds.
564    /// The demand counts any change: a nearer leg parking lowers it, and the pull
565    /// must re-decide for that leg as much as for a farther one.
566    ///
567    /// Registers the wakeup BEFORE it re-reads the frontiers. `notify_waiters` wakes
568    /// only waiters registered when it fires and stores no permit, so an advance
569    /// that lands between the caller reading the frontiers and this park would
570    /// otherwise be lost, and the pull would wait forever (#1673).
571    pub async fn past(&self, observed_paid: u64, observed_demand: u64) {
572        let notified = self.served_paid.advanced.notified();
573        tokio::pin!(notified);
574        loop {
575            notified.as_mut().enable();
576            if self.served_paid() > observed_paid || self.serve_demand() != observed_demand {
577                return;
578            }
579            notified.as_mut().await;
580            notified.set(self.served_paid.advanced.notified());
581        }
582    }
583}
584
585/// One in-flight fill of a `total_bytes`-byte blob rooted at `root`.
586///
587/// Holds a reference to the per-hash [`HashOutboard`] (captured by the cache admit
588/// path via [`Self::capture`], read by [`SessionOutboardReader`]), the pull's
589/// terminal outcome ([`Self::mark_ended`] / [`Self::outcome`]), the ranges this fill
590/// covers ([`Self::set_covered`]), and the shared downstream frontiers the pull
591/// leg paces against ([`Self::served_frontier`] / [`Self::serve_demand`], awaited
592/// through [`Self::downstream_watch`]). Shared
593/// behind an `Arc`; every field is interior-mutable, so both legs hold
594/// `Arc<FillSession>` and coordinate on it.
595#[derive(Debug)]
596pub struct FillSession {
597    /// The per-hash captured outboard. A freshly built session starts with its own,
598    /// but registration ([`FillRegistry::register_fill`] / [`FillRegistry::claim`])
599    /// swaps in the hash's canonical one via [`Self::adopt_outboard`], so all
600    /// sessions for a hash — and every reader they mint — share the ONE buffer. The
601    /// swap runs under the registry lock before the session is handed to a pull leg
602    /// or a serve leg, so no capture or read observes a stale outboard.
603    outboard: StdMutex<Arc<HashOutboard>>,
604    /// The pull leg's terminal outcome — `None` while running, `Some(Ok)` on a
605    /// clean end, `Some(Err)` on a failure.
606    ended: StdMutex<Option<Result<(), FillError>>>,
607    /// The client's PAID content frontier: the serve leg raises it after each
608    /// voucher batch commits ([`Self::advance_served`]), the pull leg's pacer reads
609    /// it to bound `pulled − served_paid ≤ window`, plus one floor to serve
610    /// `serve_demand`. A cloneable handle, so a pull leg on its own runtime can hold
611    /// an owned copy.
612    served_paid: Frontier,
613    /// The ABSOLUTE content offset the owning request starts at — the value
614    /// `served_paid` is seeded with. A pull leg's `RampPacer` ramps on
615    /// `served_paid − served_start` plus the owning stream's carried lane credit,
616    /// so a resumed request ramps from that credit, not from its blob offset.
617    served_start: u64,
618    /// Where the starved serve legs reading this fill's content wait. A serve leg's
619    /// frame consumer stands a demand through its [`DemandSlot`] when it has no
620    /// encoded bytes left and its encode is parked on a leaf or a proof node no pull
621    /// has produced ([`SessionOutboardReader::parked_on`]), and withdraws it once
622    /// the encode moves again. A park with encoded bytes still buffered is
623    /// look-ahead and stands nothing, so a client that stops paying cannot drag a
624    /// pull past its window. When the nearest demand lies within one chunk group
625    /// past a pull's frontier, that pull's pacer draws one window floor even with
626    /// its window full. Without it, a pull window that closes before the serve leg's
627    /// credit window leaves both legs waiting on each other. Shares one wakeup with
628    /// `served_paid`, so a parked pull wakes on either.
629    serve_demand: Demand,
630    /// The chunk ranges THIS fill will produce — exactly the bytes this pull
631    /// fetches (its `missing_ranges ∩ R`). [`FillRegistry::range_still_live`]
632    /// intersects a reader's node range against the union of all live sessions'
633    /// `covered` to decide whether to keep awaiting or fail. Set at registration via
634    /// [`Self::set_covered`]; defaults to [`ChunkRanges::all`] so a session built but
635    /// not yet range-scoped coalesces conservatively.
636    covered: StdMutex<ChunkRanges>,
637    /// Live observer count: the pull owner plus every serve leg attached to this
638    /// fill. An [`ObserverLease`] increments on attach and decrements on drop; when
639    /// it reaches 0 while the pull is still running ([`Self::outcome`] is `None`),
640    /// the last-out drop fires [`Self::cancel`] to stop ingest (#1610).
641    observers: AtomicUsize,
642    /// Cancels the pull when the last observer leaves before the pull ends. The
643    /// node's pull leg selects on it to abort ingest; the tagged partial persists
644    /// for a later resume.
645    cancel: CancellationToken,
646    /// A back-link to the owning registry + this session's hash, set once at
647    /// registration ([`Self::bind_registry`]). It lets a reader consult ALL
648    /// live sessions for the hash via [`FillRegistry::range_still_live`]. A session
649    /// built standalone (in tests) leaves this unset and falls back to its own
650    /// single-fill liveness — correct, because a standalone session is the only fill.
651    registry: OnceLock<(Weak<FillRegistry>, Hash)>,
652    /// The node's pull-thread [`JoinHandle`](std::thread::JoinHandle), parked here by
653    /// the owner right after it spawns the pull, so whichever observer leaves LAST
654    /// joins it. That frees an owner whose OWN client finishes first from parking in
655    /// the join while other observers still stream: a non-last-out release returns
656    /// immediately, and the true last-out [`ObserverLease`] drop takes the handle back
657    /// out to join OFF the map lock. `None` when no pull was spawned (an attach, or a
658    /// spawn that failed).
659    pull_thread: StdMutex<Option<std::thread::JoinHandle<()>>>,
660}
661
662impl FillSession {
663    /// Construct the fill session for a `total_bytes`-byte blob rooted at `root`,
664    /// with its served frontier at 0. An owning branch for a request that starts
665    /// past byte 0 uses [`Self::starting_at`] instead.
666    #[must_use]
667    pub fn new(root: blake3::Hash, total_bytes: u64) -> Arc<Self> {
668        Self::starting_at(root, total_bytes, 0)
669    }
670
671    /// Construct the fill session for a `total_bytes`-byte blob rooted at `root`,
672    /// with its served frontier at `served_start`: the ABSOLUTE content offset the
673    /// request starts at, so a non-zero-offset request shows no phantom lead. The
674    /// session starts with a private per-hash outboard, replaced by the registry's
675    /// canonical one at registration.
676    #[must_use]
677    pub fn starting_at(root: blake3::Hash, total_bytes: u64, served_start: u64) -> Arc<Self> {
678        let downstream_advanced = Arc::new(Notify::new());
679        Arc::new(Self {
680            outboard: StdMutex::new(HashOutboard::new(root, total_bytes)),
681            ended: StdMutex::new(None),
682            served_paid: Frontier::new(served_start, Arc::clone(&downstream_advanced)),
683            served_start,
684            serve_demand: Demand::new(downstream_advanced),
685            covered: StdMutex::new(ChunkRanges::all()),
686            observers: AtomicUsize::new(0),
687            cancel: CancellationToken::new(),
688            registry: OnceLock::new(),
689            pull_thread: StdMutex::new(None),
690        })
691    }
692
693    /// Park the owner's pull-thread handle on the session so whichever observer leaves
694    /// LAST joins it. Called once, on an owning branch, right after the pull thread is
695    /// spawned. The pure-attach path never calls this — it drives no pull of its own.
696    ///
697    /// Refuses to overwrite an already-parked handle: a session has exactly one owning
698    /// pull, so a second park cannot happen — but if a future regression called this
699    /// twice, overwriting would DROP the first handle and detach its thread, orphaning
700    /// a pull that keeps paying/draining. So keep the first (the one the last-out
701    /// observer will join) and `debug_assert` the double-park loudly in tests/dev.
702    pub fn set_pull_handle(&self, handle: std::thread::JoinHandle<()>) {
703        let mut slot = self
704            .pull_thread
705            .lock()
706            .unwrap_or_else(PoisonError::into_inner);
707        debug_assert!(
708            slot.is_none(),
709            "a FillSession parks exactly one pull-thread handle; a second park would \
710             detach the first and orphan its pull"
711        );
712        if slot.is_none() {
713            *slot = Some(handle);
714        }
715    }
716
717    /// Take the parked pull-thread handle, if any. Called only by the last-out lease
718    /// teardown, so exactly one caller ever receives it — the one that then joins the
719    /// thread off-task.
720    fn take_pull_handle(&self) -> Option<std::thread::JoinHandle<()>> {
721        self.pull_thread
722            .lock()
723            .unwrap_or_else(PoisonError::into_inner)
724            .take()
725    }
726
727    /// The per-hash outboard this session currently references. Cloned out (a cheap
728    /// `Arc` bump) rather than borrowed, so the swap lock is never held across a
729    /// capture or a read.
730    fn outboard(&self) -> Arc<HashOutboard> {
731        self.outboard
732            .lock()
733            .unwrap_or_else(PoisonError::into_inner)
734            .clone()
735    }
736
737    /// Adopt the hash's canonical outboard (shared with sibling sessions). Called
738    /// once, under the registry lock, before this session drives a pull or mints a
739    /// reader — so every later capture / read lands in the one shared buffer.
740    fn adopt_outboard(&self, shared: Arc<HashOutboard>) {
741        *self.outboard.lock().unwrap_or_else(PoisonError::into_inner) = shared;
742    }
743
744    /// Bind this session to its registry + hash, so a reader it mints can ask the
745    /// registry which live fills still cover a range. Called once at registration.
746    fn bind_registry(&self, registry: Weak<FillRegistry>, hash: Hash) {
747        let _ = self.registry.set((registry, hash));
748    }
749
750    /// Set the chunk ranges this fill will produce. Called once at registration,
751    /// before the pull streams — the registry reads it to plan observer attach and
752    /// to answer [`Self::range_still_live`].
753    pub fn set_covered(&self, covered: ChunkRanges) {
754        *self.covered.lock().unwrap_or_else(PoisonError::into_inner) = covered;
755    }
756
757    /// A clone of this fill's `covered` ranges.
758    fn covered_ranges(&self) -> ChunkRanges {
759        self.covered
760            .lock()
761            .unwrap_or_else(PoisonError::into_inner)
762            .clone()
763    }
764
765    /// The blob's total byte length. The attach path signs its `StreamResponse`
766    /// from this without re-running the upstream header handshake — the session
767    /// already knows the geometry from its `BaoTree`.
768    #[must_use]
769    pub fn total_bytes(&self) -> u64 {
770        self.outboard().tree.size()
771    }
772
773    /// The ABSOLUTE content offset this session's owning request starts at.
774    #[must_use]
775    pub const fn served_start(&self) -> u64 {
776        self.served_start
777    }
778
779    /// The current PAID content frontier (absolute): how far the observers of
780    /// this fill have paid, and therefore how far past it the pull may run.
781    #[must_use]
782    pub fn served_paid(&self) -> u64 {
783        self.served_paid.get()
784    }
785
786    /// The live observer count (pull owner + attached serve legs).
787    #[must_use]
788    pub fn observer_count(&self) -> usize {
789        self.observers.load(Ordering::Acquire)
790    }
791
792    /// Whether the pull has been cancelled because the last observer left.
793    #[must_use]
794    pub fn is_cancelled(&self) -> bool {
795        self.cancel.is_cancelled()
796    }
797
798    /// The pull leg's cancellation token — it selects on this to abort ingest when
799    /// the last observer leaves before the pull completes.
800    #[must_use]
801    pub const fn cancel_token(&self) -> &CancellationToken {
802        &self.cancel
803    }
804
805    /// Whether this session is dead: the pull has recorded a terminal outcome, or
806    /// the fill was cancelled because its last observer left. A dead session must
807    /// not block a fresh pull (its `covered` is excluded from the coverage union)
808    /// and must not be an attach target.
809    #[must_use]
810    fn is_dead(&self) -> bool {
811        self.outcome().is_some() || self.cancel.is_cancelled()
812    }
813
814    /// Mint a serve-side [`SessionOutboardReader`] over this hash's shared outboard.
815    /// The reader snapshots the (already-adopted) outboard and holds this session so
816    /// its termination race can reach the registry. A serve path attaches the store
817    /// ([`SessionOutboardReader::with_store_fallback`]); without it, a load of a node
818    /// no live fill captures fails.
819    #[must_use]
820    pub fn outboard_reader(self: &Arc<Self>) -> SessionOutboardReader {
821        SessionOutboardReader {
822            session: Arc::clone(self),
823            outboard: self.outboard(),
824            parked_on: Arc::new(AtomicU64::new(0)),
825            store: None,
826        }
827    }
828
829    /// The shared PAID content frontier the pull leg's pacer bounds against.
830    #[must_use]
831    pub const fn served_frontier(&self) -> &Frontier {
832        &self.served_paid
833    }
834
835    /// Raise the PAID content frontier to `served` after a downstream voucher batch
836    /// commits, waking the parked pull if it moved. A lower value changes nothing,
837    /// so N observers advancing one shared frontier never regress each other.
838    pub fn advance_served(&self, served: u64) {
839        self.served_paid.raise(served);
840    }
841
842    /// Raise the PAID content frontier to `served` on behalf of an observer whose
843    /// delivery starts at content `offset`, but only when this session's frontier
844    /// already reaches `offset`.
845    ///
846    /// Under partial-overlap coalescing an observer consumes a SUFFIX of this fill's
847    /// covered range, starting at `offset`. Its payment extends this fill's paid
848    /// PREFIX only once that prefix reaches `offset`. Before that, the bytes in
849    /// `[frontier, offset)` are unpaid, and raising the frontier past them would let
850    /// the pull run its window over bytes no observer of this fill has paid for.
851    pub fn extend_served_from(&self, offset: u64, served: u64) {
852        if self.served_paid.get() >= offset {
853            self.served_paid.raise(served);
854        }
855    }
856
857    /// A read-only watch over both downstream frontiers, for a pull leg to pace
858    /// against and park on.
859    #[must_use]
860    pub fn downstream_watch(&self) -> DownstreamWatch {
861        DownstreamWatch {
862            served_paid: self.served_paid.clone(),
863            serve_demand: self.serve_demand.clone(),
864        }
865    }
866
867    /// The serve demand of this fill. When its nearest value lies within one chunk
868    /// group past a pull's frontier, that pull's pacer draws one window floor even
869    /// with its window full; a demand further out is ignored.
870    #[must_use]
871    pub const fn serve_demand(&self) -> &Demand {
872        &self.serve_demand
873    }
874
875    /// Mint the [`DemandSlot`] one serve leg reading this fill stands its demand
876    /// through. Under coalescing the awaited byte may belong to a sibling fill of
877    /// the hash, so the slot routes each demand through the registry.
878    #[must_use]
879    pub fn demand_slot(self: &Arc<Self>) -> DemandSlot {
880        DemandSlot {
881            session: Arc::clone(self),
882            id: NEXT_DEMAND_SLOT.fetch_add(1, Ordering::Relaxed),
883        }
884    }
885
886    /// The owning registry and this session's hash, when registered and the
887    /// registry is still alive.
888    fn registry_and_hash(&self) -> Option<(Arc<FillRegistry>, Hash)> {
889        self.registry
890            .get()
891            .and_then(|(weak, hash)| weak.upgrade().map(|registry| (registry, *hash)))
892    }
893
894    /// Stand slot `slot`'s demand for content up to `end` here when this live fill
895    /// produces `byte`, and withdraw it otherwise.
896    fn route_own_demand(&self, slot: u64, byte: u64, end: u64) {
897        let chunk = bao_tree::ChunkNum::full_chunks(byte);
898        let produces = !self.is_dead() && {
899            let covered = self.covered.lock().unwrap_or_else(PoisonError::into_inner);
900            !(&*covered & &ChunkRanges::from(chunk..chunk + 1)).is_empty()
901        };
902        if produces {
903            self.serve_demand.stand(slot, end);
904        } else {
905            self.serve_demand.withdraw(slot);
906        }
907    }
908
909    /// The per-hash liveness signal, notified whenever any session for this hash
910    /// ends or is cancelled. A serve-side data reader awaits it (alongside the
911    /// present-range watch) so a read of a range no live pull will fill settles
912    /// rather than hanging: served from the store when it holds the bytes, failed
913    /// otherwise. Handed out as an owned `Arc<Notify>` so the node's data reader
914    /// need not name the cache-private [`HashOutboard`].
915    #[must_use]
916    pub fn liveness_signal(&self) -> Arc<Notify> {
917        Arc::clone(&self.outboard().liveness)
918    }
919
920    /// Whether any LIVE fill of this hash still covers `range`, so a not-yet-present
921    /// node / leaf in `range` may still be filled. A registry-bound session consults
922    /// EVERY live session for the hash (the N-fill generalization); a standalone
923    /// session (unregistered, in tests) is the only fill, so it answers from its own
924    /// liveness. Returns `false` when nothing live covers `range` — the caller then
925    /// fails the read instead of hanging.
926    #[must_use]
927    pub fn range_still_live(&self, range: &ChunkRanges) -> bool {
928        match self.registry.get() {
929            Some((weak, hash)) => match weak.upgrade() {
930                Some(registry) => registry.range_still_live(*hash, range),
931                // The registry (the whole cache) is gone; nothing can fill anything.
932                None => false,
933            },
934            // Standalone: this session is the only fill, so its liveness decides.
935            None => !self.is_dead() && !(&self.covered_ranges() & range).is_empty(),
936        }
937    }
938
939    /// Capture one internal node's `(left, right)` hash pair into the per-hash
940    /// outboard. Idempotent; a leaf (no outboard slot) is ignored. Every
941    /// `admit_bao_stream` for the hash, from any pull or source, calls this — so the
942    /// one shared buffer accumulates the whole tree's proof.
943    pub fn capture(&self, node: TreeNode, pair: (blake3::Hash, blake3::Hash)) {
944        self.outboard().capture(node, pair);
945    }
946
947    /// Capture a batch of node pairs into the per-hash outboard in one shot, waking
948    /// parked serve legs once for the batch rather than once per node. The admit path
949    /// hands each leaf's run of proof nodes here just before the leaf, so a serve leg
950    /// reads a leaf's proof as soon as that leaf arrives, with at most one wake per
951    /// leaf.
952    pub fn capture_many(
953        &self,
954        pairs: impl IntoIterator<Item = (TreeNode, (blake3::Hash, blake3::Hash))>,
955    ) {
956        self.outboard().capture_many(pairs);
957    }
958
959    /// Record the pull leg's terminal outcome and wake parked readers. Idempotent
960    /// on the wake; the outcome is set once by the pull leg on its single exit.
961    /// Fires the per-hash liveness so a reader awaiting a range this fill covered
962    /// re-checks [`Self::range_still_live`] at once.
963    pub fn mark_ended(&self, result: Result<(), FillError>) {
964        {
965            let mut guard = self.ended.lock().unwrap_or_else(PoisonError::into_inner);
966            *guard = Some(result);
967        }
968        self.outboard().liveness.notify_waiters();
969    }
970
971    /// The pull leg's terminal outcome, if it has ended: `Some(Ok)` clean,
972    /// `Some(Err)` failed. `None` while the pull is still running.
973    #[must_use]
974    pub fn outcome(&self) -> Option<Result<(), FillError>> {
975        self.ended
976            .lock()
977            .unwrap_or_else(PoisonError::into_inner)
978            .clone()
979    }
980}
981
982/// Serve-side [`Outboard`] over a hash's shared [`HashOutboard`]. Its
983/// [`Outboard::load`] awaits a not-yet-captured node, racing the registry-wide fill
984/// liveness for the no-hang guarantee: once no live fill still covers the node's
985/// byte range, it reads the node from the local store when the reader carries a
986/// store fallback ([`Self::with_store_fallback`]), and fails when that cannot
987/// supply it. Minted via [`FillSession::outboard_reader`].
988#[derive(Debug)]
989pub struct SessionOutboardReader {
990    session: Arc<FillSession>,
991    /// The shared per-hash outboard, snapshot at mint (after the session adopted the
992    /// canonical one), so every `load` reads whatever any pull for the hash captured.
993    outboard: Arc<HashOutboard>,
994    /// The content end this reader waits on, written before each park: one byte
995    /// into the node whose pair no pull has captured. The reader does NOT raise
996    /// serve demand itself — an encoder reads ahead of what its consumer needs, so
997    /// a park here alone does not mean the serve leg is stuck. The frame consumer
998    /// stands this value via its [`DemandSlot`] only once it is
999    /// starved of encoded bytes ([`Self::parked_on`]).
1000    parked_on: Arc<AtomicU64>,
1001    /// The local store, read for a node no live fill will capture
1002    /// ([`Self::with_store_fallback`]). `None` fails such a load.
1003    store: Option<StoreFallback>,
1004}
1005
1006/// The local store a [`SessionOutboardReader`] reads a node's pair from when no
1007/// live fill will capture it.
1008#[derive(Debug)]
1009struct StoreFallback {
1010    engine: CacheEngine,
1011    /// The store's present chunks of the hash as last read. A node they already
1012    /// meet needs no fresh read.
1013    present: ChunkRanges,
1014}
1015
1016impl StoreFallback {
1017    /// Capture into `outboard` the pairs `export_bao` emits for ONE chunk group the
1018    /// store holds under a node of chunk range `node_range`: every parent on the
1019    /// group's proof path, the node among them, plus the right-siblings over
1020    /// absent content. The store reads and verifies at most that one group's
1021    /// leaves, however much content the node spans. Captures nothing when the
1022    /// store holds nothing under the node. Returns whether the export yielded any
1023    /// pairs.
1024    async fn capture(
1025        &mut self,
1026        outboard: &HashOutboard,
1027        node_range: &ChunkRanges,
1028    ) -> CacheResult<bool> {
1029        let hash = Hash::from_bytes(*outboard.root.as_bytes());
1030        if (node_range & &self.present).is_empty() {
1031            self.present = self
1032                .engine
1033                .present_ranges(hash)
1034                .await?
1035                .chunk_ranges()
1036                .clone();
1037        }
1038        let held = node_range & &self.present;
1039        let Some(first) = held.boundaries().first().copied() else {
1040            return Ok(false);
1041        };
1042        let group_chunks = 1u64 << IROH_BLOCK_SIZE.chunk_log();
1043        let group_start = first.0 / group_chunks * group_chunks;
1044        let group = ChunkRanges::from(
1045            ChunkNum(group_start)..ChunkNum(group_start.saturating_add(group_chunks)),
1046        );
1047        let pairs = self.engine.outboard_pairs(hash, &(&held & &group)).await?;
1048        let exported = !pairs.is_empty();
1049        outboard.capture_many(pairs);
1050        Ok(exported)
1051    }
1052}
1053
1054impl SessionOutboardReader {
1055    /// Read a node no live fill will capture from `engine`, the local store, before
1056    /// failing the load. The store can hold content under such a node: a pull that
1057    /// faulted landed part of its range first, or an earlier fill stored it. The
1058    /// store's outboard then holds the node's pair. Without the fallback such a
1059    /// load fails.
1060    #[must_use]
1061    pub fn with_store_fallback(mut self, engine: CacheEngine) -> Self {
1062        self.store = Some(StoreFallback {
1063            engine,
1064            present: ChunkRanges::empty(),
1065        });
1066        self
1067    }
1068
1069    /// The cell this reader writes the content end it waits on into. A frame
1070    /// consumer shares it with its data reader and stands it as serve demand
1071    /// while it has no encoded bytes left to take.
1072    #[must_use]
1073    pub fn parked_on(&self) -> Arc<AtomicU64> {
1074        Arc::clone(&self.parked_on)
1075    }
1076
1077    /// Log a node the store fallback supplied. After a failed pull, or once
1078    /// every covering pull ended, the store is the expected source: debug. After
1079    /// this session's own pull completed cleanly, the pull should have captured
1080    /// the node itself, so the fallback covers a capture defect: warn.
1081    fn log_store_hit(&self, node: TreeNode) {
1082        let hash = self.outboard.root;
1083        if matches!(self.session.outcome(), Some(Ok(()))) {
1084            tracing::warn!(
1085                %hash,
1086                ?node,
1087                "a clean pull never captured an outboard node; read it from the store"
1088            );
1089        } else {
1090            tracing::debug!(
1091                %hash,
1092                ?node,
1093                "read an outboard node no live fill will capture from the store"
1094            );
1095        }
1096    }
1097
1098    /// The terminal error for a node no live fill will supply and the store
1099    /// does not hold: a precise message when this session's own pull ended (the
1100    /// standalone / single-fill case), a generic one when the covering fills
1101    /// were siblings. Each is [`is_unlanded`].
1102    fn dead_range_error(&self, node: TreeNode) -> io::Error {
1103        match self.session.outcome() {
1104            Some(Err(msg)) => msg.unlanded(format!(
1105                "upstream pull failed before supplying outboard node {node:?}"
1106            )),
1107            Some(Ok(())) => unlanded(format!(
1108                "upstream pull completed but outboard node {node:?} was never captured"
1109            )),
1110            None => unlanded(format!(
1111                "no live fill covers outboard node {node:?}; every covering pull ended"
1112            )),
1113        }
1114    }
1115}
1116
1117impl Outboard for SessionOutboardReader {
1118    fn root(&self) -> blake3::Hash {
1119        self.outboard.root
1120    }
1121
1122    fn tree(&self) -> BaoTree {
1123        self.outboard.tree
1124    }
1125
1126    async fn load(&mut self, node: TreeNode) -> io::Result<Option<(blake3::Hash, blake3::Hash)>> {
1127        let Some(offset) = self.outboard.tree.pre_order_offset(node) else {
1128            return Ok(None); // leaf: bao_tree sources this from the data reader
1129        };
1130        let idx = usize::try_from(offset).unwrap_or(usize::MAX);
1131        let node_range = ChunkRanges::from(node.chunk_range());
1132        loop {
1133            // Register the wakers BEFORE inspecting shared state, so a capture /
1134            // fill-end recorded concurrently cannot slip between the check and the
1135            // await.
1136            let captured = self.outboard.captured.notified();
1137            let liveness = self.outboard.liveness.notified();
1138            tokio::pin!(captured);
1139            tokio::pin!(liveness);
1140            captured.as_mut().enable();
1141            liveness.as_mut().enable();
1142
1143            if let Some(pair) = self.outboard.try_load(idx) {
1144                return Ok(Some(pair));
1145            }
1146
1147            // Not captured yet. If no live fill still covers this node's range, no
1148            // pull will capture the node — decide now. A clean end may lag one
1149            // capture behind the outcome, so re-check once. Then read the node
1150            // from the store, which may hold content under it that a fill landed
1151            // before it ended, and fail only when the store cannot supply it.
1152            if !self.session.range_still_live(&node_range) {
1153                if let Some(pair) = self.outboard.try_load(idx) {
1154                    return Ok(Some(pair));
1155                }
1156                let exported = match self.store.as_mut() {
1157                    Some(fallback) => fallback.capture(&self.outboard, &node_range).await,
1158                    None => Ok(false),
1159                };
1160                match exported {
1161                    Ok(exported) => {
1162                        if let Some(pair) = self.outboard.try_load(idx) {
1163                            if exported {
1164                                self.log_store_hit(node);
1165                            }
1166                            return Ok(Some(pair));
1167                        }
1168                        if exported {
1169                            return Err(io::Error::other(format!(
1170                                "the store holds content under outboard node {node:?} but \
1171                                 its export did not yield the node"
1172                            )));
1173                        }
1174                    }
1175                    Err(err) => {
1176                        return Err(io::Error::other(format!(
1177                            "{}; the store read for it failed: {}",
1178                            self.dead_range_error(node),
1179                            ErrorChain::new(&err)
1180                        )));
1181                    }
1182                }
1183                return Err(self.dead_range_error(node));
1184            }
1185
1186            // A pull captures this pair only once it fetches into the node's range.
1187            // Record the first byte of that range as what this encode waits on; the
1188            // frame consumer turns it into serve demand if it starves on this park.
1189            let node_start = node.chunk_range().start.to_bytes();
1190            self.parked_on
1191                .store(node_start.saturating_add(1), Ordering::Relaxed);
1192
1193            // Await the next capture or a liveness change, then re-check.
1194            tokio::select! {
1195                biased;
1196                () = liveness.as_mut() => {}
1197                () = captured.as_mut() => {}
1198            }
1199        }
1200    }
1201}
1202
1203/// RAII observer handle on a [`FillSession`]. One is minted for the pull owner and
1204/// one per attached serve leg ([`FillRegistry::claim`]). Releasing it decrements the
1205/// session's observer count; the last-out release, if the pull is still running,
1206/// cancels the pull so a fill no client is waiting on stops ingesting (#1610) — and
1207/// hands back the session's parked pull-thread handle so THAT caller joins it
1208/// off-task (the owner-join hand-off: whoever leaves LAST joins the pull, freeing an
1209/// owner whose own client finished first).
1210///
1211/// The decrement + cancel decision runs UNDER the registry map lock — the same
1212/// lock `claim`/`register_fill` hold when they attach an observer. That mutual
1213/// exclusion closes the resurrection race: a concurrent claim cannot `fetch_add` a
1214/// session 0→1 in the window between this drop's last-out `fetch_sub` (1→0) and its
1215/// `cancel()`, so no live observer can end up bound to a cancelled fill. The lease
1216/// therefore holds a [`Weak`] back to the registry (plus the session's `hash`) to
1217/// reach that lock at drop. If the upgrade fails the registry is gone, so the fill
1218/// is moot and the drop just skips. It only TAKES the pull handle under the lock;
1219/// the JOIN always runs off it.
1220#[derive(Debug)]
1221pub struct ObserverLease {
1222    session: Arc<FillSession>,
1223    registry: Weak<FillRegistry>,
1224    hash: Hash,
1225    /// Cleared once teardown has run. [`Self::release`] consumes the lease and runs
1226    /// teardown explicitly; the value then still `Drop`s, so this flag makes the
1227    /// second pass a no-op — the decrement fires exactly once.
1228    armed: bool,
1229}
1230
1231impl ObserverLease {
1232    /// Release the lease at a serve leg's teardown, handing back the parked
1233    /// pull-thread handle when THIS release is the last one out. The caller joins that
1234    /// handle OFF the registry map lock and off its accept task (on a blocking
1235    /// thread). A non-last-out release returns `None`, so an owner whose own client
1236    /// finished first returns at once, leaving the pull filling for the remaining
1237    /// observers — the last of which cancels it (#1610 preserved). Named so teardown
1238    /// reads as intent rather than an incidental drop.
1239    #[must_use]
1240    pub fn release(mut self) -> Option<std::thread::JoinHandle<()>> {
1241        self.teardown()
1242    }
1243
1244    /// The last-out decrement + cancel, shared by [`Self::release`] and [`Drop`].
1245    /// Runs under the registry map lock — the SAME lock `claim`/`register_fill` hold
1246    /// to attach — so the decrement, the map removal, and the cancel decision cannot
1247    /// interleave with an attach (the #1610 resurrection-race fix). It only TAKES the
1248    /// pull handle here; it never JOINS under the lock. Idempotent via `armed`.
1249    fn teardown(&mut self) -> Option<std::thread::JoinHandle<()>> {
1250        if !self.armed {
1251            return None;
1252        }
1253        self.armed = false;
1254        let Some(registry) = self.registry.upgrade() else {
1255            // Registry dropped — the fill (and its map entry) are gone; nothing can
1256            // attach, so the decrement + cancel are moot. Skip.
1257            return None;
1258        };
1259        // Take the SAME lock `claim`/`register_fill` hold to attach, so the
1260        // decrement, the map removal, and the cancel decision cannot interleave with
1261        // an attach.
1262        let mut map = registry.map.lock().unwrap_or_else(PoisonError::into_inner);
1263        // `fetch_sub` returns the PREVIOUS value; `1` means this drop took the
1264        // count to 0.
1265        let prev = self.session.observers.fetch_sub(1, Ordering::AcqRel);
1266        if prev != 1 {
1267            return None;
1268        }
1269        // Last observer left: free the session from the map so dead sessions do not
1270        // accumulate forever. Remove by pointer identity so a sibling session for the
1271        // same hash survives; drop the `hash` key if its Vec empties (the per-hash
1272        // outboard then drops once every reader releases it).
1273        if let Some(entry) = map.get_mut(&self.hash) {
1274            entry.sessions.retain(|s| !Arc::ptr_eq(s, &self.session));
1275            if entry.sessions.is_empty() {
1276                map.remove(&self.hash);
1277            }
1278        }
1279        // Cancel only if the pull has not already ended — a completed pull needs no
1280        // cancel, and a failed one already terminated. The session is removed from the
1281        // map regardless. Firing the per-hash liveness wakes any reader parked on a
1282        // range this fill covered so it re-checks at once.
1283        if self.session.outcome().is_none() {
1284            self.session.cancel.cancel();
1285        }
1286        self.session.outboard().liveness.notify_waiters();
1287        // Hand the parked pull-thread handle to this last-out caller to join off-task.
1288        // Taking (not joining) it here keeps the map lock free of a blocking join.
1289        self.session.take_pull_handle()
1290    }
1291}
1292
1293impl Drop for ObserverLease {
1294    fn drop(&mut self) {
1295        let _ = self.teardown();
1296    }
1297}
1298
1299/// The atomic outcome of [`FillRegistry::claim`]: a serve-miss either coalesces
1300/// wholly onto live pulls (`Attach`), owns a fresh pull for its whole range
1301/// (`Owner`), or — on a partial overlap — owns a fresh pull for its non-overlapping
1302/// REMAINDER while attaching to a live sibling for the overlap (`Mixed`). Every
1303/// variant carries the [`FillSession`]s and [`ObserverLease`]s the caller holds
1304/// for the life of its serve leg. `claim` decides the split AND registers any new
1305/// owner session under ONE map-lock acquisition, so two concurrent fresh misses for
1306/// the same blob cannot both open a pull.
1307#[derive(Debug)]
1308pub enum FillClaim {
1309    /// A live pull already covers the whole request; run a serve leg over `session`
1310    /// and open NO new pull. `lease` is an observer lease on the existing fill.
1311    Attach {
1312        /// The live session to serve from (largest overlap with the request).
1313        session: Arc<FillSession>,
1314        /// The observer lease held for the life of the attaching serve leg.
1315        lease: ObserverLease,
1316    },
1317    /// No live pull covers the request; the caller owns the freshly-registered
1318    /// `session` (scoped to the WHOLE request) and must spawn the pull. `lease` is
1319    /// the owner lease (observer count starts at 1).
1320    Owner {
1321        /// The freshly-registered session the caller must drive a pull for.
1322        session: Arc<FillSession>,
1323        /// The owner lease (count 1) held for the life of the owning pull.
1324        lease: ObserverLease,
1325    },
1326    /// A partial overlap: a live sibling covers a prefix/suffix of the request. The
1327    /// caller OWNS a fresh pull for the contiguous `remainder` (`[remainder_offset,
1328    /// remainder_offset + remainder_len)`) and ATTACHES to the sibling for the
1329    /// overlap, serving the whole request from the shared per-hash cache + outboard.
1330    /// Two pulls, each fetching its bytes once.
1331    Mixed {
1332        /// The freshly-registered session covering the remainder; drive its pull.
1333        owner: Arc<FillSession>,
1334        /// The owner lease (count 1) for the remainder pull.
1335        owner_lease: ObserverLease,
1336        /// The live sibling covering the overlap; its pull is shared, not re-opened.
1337        attach: Arc<FillSession>,
1338        /// The observer lease on the sibling, held for the life of the serve leg.
1339        attach_lease: ObserverLease,
1340        /// The remainder's byte offset — the pull leg fetches `[offset, offset+len)`.
1341        remainder_offset: u64,
1342        /// The remainder's byte length.
1343        remainder_len: u64,
1344    },
1345}
1346
1347/// Total chunk count spanned by a (disjoint, sorted) chunk-range set. Boundaries
1348/// come in `[start, end)` pairs; a trailing unpaired boundary (an open-ended set)
1349/// contributes nothing, which is fine for the finite `covered ∩ R` overlaps this
1350/// measures.
1351fn chunk_span(ranges: &ChunkRanges) -> u64 {
1352    let (pairs, _open_end) = ranges.boundaries().as_chunks::<2>();
1353    pairs
1354        .iter()
1355        .map(|[start, end]| end.0.saturating_sub(start.0))
1356        .sum()
1357}
1358
1359/// The single contiguous byte span `[offset, offset+len)` of a chunk-range set, or
1360/// `None` if it is empty or split into two or more disjoint pieces. A `Mixed` claim
1361/// only opens a remainder pull when the remainder is one contiguous range (a
1362/// prefix/suffix overlap), because the pull leg fetches one `[offset, len)` gap; a
1363/// remainder split by an interior overlap falls back to a conservative whole-request
1364/// pull. `total` clamps the final (possibly ragged) group to the blob end.
1365fn contiguous_byte_span(ranges: &ChunkRanges, total: u64) -> Option<(u64, u64)> {
1366    match ranges.boundaries() {
1367        [start, end] => {
1368            let offset = start.0.saturating_mul(CHUNK_BYTES);
1369            let end_bytes = end.0.saturating_mul(CHUNK_BYTES).min(total);
1370            Some((offset, end_bytes.saturating_sub(offset)))
1371        }
1372        _ => None,
1373    }
1374}
1375
1376/// One hash's live in-flight fills plus the ONE per-hash outboard they all capture
1377/// into and serve from.
1378#[derive(Debug)]
1379struct HashEntry {
1380    /// The canonical per-hash outboard, adopted by every session for this hash.
1381    outboard: Arc<HashOutboard>,
1382    /// The live fills for this hash (each a distinct pull). Removed by pointer
1383    /// identity when a session's last observer leaves.
1384    sessions: Vec<Arc<FillSession>>,
1385}
1386
1387/// Range-aware coalescing registry: the live in-flight fills, keyed by hash, plus a
1388/// per-hash captured outboard. A serve-miss consults [`Self::claim`] to split its
1389/// range into an attach (covered by a running pull) and a remainder (fetch it), and
1390/// [`Self::range_still_live`] answers a parked reader's "keep awaiting or fail?".
1391/// Purely synchronous range math under a std `Mutex`; the lock is never held across
1392/// an await.
1393#[derive(Debug, Default)]
1394pub struct FillRegistry {
1395    map: StdMutex<HashMap<Hash, HashEntry>>,
1396    /// Cache metrics for the coalescing decisions this registry makes; `None`
1397    /// in unit fixtures.
1398    metrics: Option<Arc<crate::metrics::CacheMetrics>>,
1399}
1400
1401impl FillRegistry {
1402    /// An empty registry with no metrics handle (unit fixtures).
1403    #[must_use]
1404    pub fn new() -> Self {
1405        Self::with_metrics(None)
1406    }
1407
1408    /// An empty registry that reports its coalescing decisions to `metrics`.
1409    #[must_use]
1410    pub fn with_metrics(metrics: Option<Arc<crate::metrics::CacheMetrics>>) -> Self {
1411        Self {
1412            map: StdMutex::new(HashMap::new()),
1413            metrics,
1414        }
1415    }
1416
1417    /// Increment `session`'s observer count and mint its RAII lease. The caller
1418    /// MUST already hold the map lock — the increment shares the lock the lease's
1419    /// drop takes for its decrement, so attach and teardown are mutually exclusive.
1420    fn mint_lease(self: &Arc<Self>, session: &Arc<FillSession>, hash: Hash) -> ObserverLease {
1421        session.observers.fetch_add(1, Ordering::AcqRel);
1422        ObserverLease {
1423            session: Arc::clone(session),
1424            registry: Arc::downgrade(self),
1425            hash,
1426            armed: true,
1427        }
1428    }
1429
1430    /// Insert a freshly-built `session` for `hash` under the held map lock: adopt the
1431    /// hash's canonical outboard (or seed it from this session if it is the first),
1432    /// bind the session's registry back-link, and publish it. The session's
1433    /// `covered` must already be set by the caller.
1434    fn insert_session(
1435        self: &Arc<Self>,
1436        map: &mut HashMap<Hash, HashEntry>,
1437        hash: Hash,
1438        session: &Arc<FillSession>,
1439    ) {
1440        let entry = map.entry(hash).or_insert_with(|| HashEntry {
1441            outboard: session.outboard(),
1442            sessions: Vec::new(),
1443        });
1444        session.adopt_outboard(Arc::clone(&entry.outboard));
1445        session.bind_registry(Arc::downgrade(self), hash);
1446        entry.sessions.push(Arc::clone(session));
1447    }
1448
1449    /// Publish a new pull's `session` (already scoped via [`FillSession::set_covered`]
1450    /// to the bytes it will fetch) and return the owner lease (observer count starts
1451    /// at 1). The entry stays in the map until the owning pull is joined node-side.
1452    /// A test-facing helper; production registers via [`Self::claim`].
1453    pub fn register_fill(
1454        self: &Arc<Self>,
1455        hash: Hash,
1456        session: &Arc<FillSession>,
1457    ) -> ObserverLease {
1458        let mut map = self.map.lock().unwrap_or_else(PoisonError::into_inner);
1459        self.insert_session(&mut map, hash, session);
1460        self.mint_lease(session, hash)
1461    }
1462
1463    /// The blob length of a LIVE in-flight fill for `hash`, if one runs — an
1464    /// ADVISORY peek the serve-miss path uses to skip its upstream header handshake
1465    /// when a pull it can coalesce onto already exists. Returns the `total_bytes` of
1466    /// the first non-dead session under `hash` (every session for one hash shares
1467    /// the blob's geometry, so any live one's length is THE length); `None` when no
1468    /// live fill exists (empty, or only dead sessions remain). Advisory only: a
1469    /// concurrent last-observer drop can retire the peeked session before the
1470    /// caller's atomic [`Self::claim`], which then owns a fresh pull and opens the
1471    /// leg late — so this NEVER replaces `claim`, it only removes the handshake in
1472    /// the common coalescing case.
1473    #[must_use]
1474    pub fn in_flight_total(&self, hash: Hash) -> Option<u64> {
1475        let map = self.map.lock().unwrap_or_else(PoisonError::into_inner);
1476        map.get(&hash)?
1477            .sessions
1478            .iter()
1479            .find(|session| !session.is_dead())
1480            .map(|session| session.total_bytes())
1481    }
1482
1483    /// Route slot `slot`'s demand for content up to `end` to the live fill of `hash`
1484    /// that produces `byte`, and withdraw it from every other fill of the hash
1485    /// ([`DemandSlot::stand`]).
1486    fn route_demand(&self, hash: Hash, slot: u64, byte: u64, end: u64) {
1487        let map = self.map.lock().unwrap_or_else(PoisonError::into_inner);
1488        if let Some(entry) = map.get(&hash) {
1489            for session in &entry.sessions {
1490                session.route_own_demand(slot, byte, end);
1491            }
1492        }
1493    }
1494
1495    /// Withdraw slot `slot`'s demand from every fill of `hash`.
1496    fn withdraw_demand(&self, hash: Hash, slot: u64) {
1497        let map = self.map.lock().unwrap_or_else(PoisonError::into_inner);
1498        if let Some(entry) = map.get(&hash) {
1499            for session in &entry.sessions {
1500                session.serve_demand.withdraw(slot);
1501            }
1502        }
1503    }
1504
1505    /// Whether any LIVE fill of `hash` still covers `range`. A parked reader calls
1506    /// this (via [`FillSession::range_still_live`]) to decide "keep awaiting a
1507    /// capture / present-range advance, or fail because no pull will ever fill this".
1508    /// `true` iff some live session's `covered` intersects `range`.
1509    #[must_use]
1510    pub fn range_still_live(&self, hash: Hash, range: &ChunkRanges) -> bool {
1511        let map = self.map.lock().unwrap_or_else(PoisonError::into_inner);
1512        match map.get(&hash) {
1513            Some(entry) => entry
1514                .sessions
1515                .iter()
1516                .any(|s| !s.is_dead() && !(&s.covered_ranges() & range).is_empty()),
1517            None => false,
1518        }
1519    }
1520
1521    /// Atomically decide attach/own/mixed AND register any new owner session, all
1522    /// under ONE map-lock acquisition. This closes the coalescing TOCTOU race a
1523    /// separate plan-then-register pair would leave open: between two lock
1524    /// acquisitions another thread can register, so two concurrent fresh misses for
1525    /// the same blob would both see an empty registry and both open a pull. `claim`
1526    /// holds the lock across the whole decision, so exactly one of two racing
1527    /// identical claims registers an owner and the other attaches.
1528    ///
1529    /// `R = align_range_clamped(offset, len, total)`: an end past `total` clamps to
1530    /// it, so a request whose claimed size grows still coalesces
1531    /// with a live fill instead of always falling to Owner. On an align error (a
1532    /// start at or past `total`) or empty `R`, the caller takes the Owner path (its
1533    /// own fetch surfaces any out-of-bounds error).
1534    /// Otherwise, over LIVE sessions **whose paid frontier reaches `R`'s aligned
1535    /// fetch start** (a session behind that frontier is excluded — attaching to it
1536    /// would park this request on a pull that advances only as the OTHER client
1537    /// pays, #2062; the exclusion trades duplicate egress for the overlap, counted
1538    /// as `fill_not_coalesced`), `covered_union = ⋃ covered`,
1539    /// `attach = R ∩ covered_union`, `remainder = R − covered_union`:
1540    /// - `attach` empty → OWNER of the whole `R`.
1541    /// - `remainder` empty (a live pull covers all of `R`) → ATTACH to the
1542    ///   largest-overlap session; `make_session` is NOT called.
1543    /// - both non-empty, and the largest-overlap sibling covers ALL of `attach`, and
1544    ///   `R − sibling.covered` is ONE contiguous span → MIXED: own a pull for that
1545    ///   remainder, attach the sibling for the overlap.
1546    /// - otherwise (a multi-sibling union, or an interior overlap splitting the
1547    ///   remainder in two) → OWNER of the whole `R`, the conservative fallback.
1548    ///   It never wedges; like the frontier exclusion above it can double-pull
1549    ///   an overlap a live sibling also covers.
1550    ///
1551    /// `make_session` runs under the lock only on an owning branch (never
1552    /// built-and-dropped on a pure attach); it does no await (a std lock) and only
1553    /// allocates, so holding the lock across it is safe.
1554    pub fn claim(
1555        self: &Arc<Self>,
1556        hash: Hash,
1557        offset: u64,
1558        len: u64,
1559        total: u64,
1560        make_session: impl FnOnce() -> Arc<FillSession>,
1561    ) -> FillClaim {
1562        let mut map = self.map.lock().unwrap_or_else(PoisonError::into_inner);
1563
1564        // R = the chunk ranges the request spans. An align error (a start at or
1565        // past the blob end) or an empty R has no coalescable range, so fall
1566        // straight to the Owner path. An end past the blob clamps instead.
1567        let (r, fetch_start) = match align_range_clamped(offset, len, total) {
1568            Ok(aligned) => (aligned.chunk_ranges().clone(), aligned.fetch_start()),
1569            Err(_) => (ChunkRanges::empty(), 0),
1570        };
1571
1572        if !r.is_empty() {
1573            let mut covered_union = ChunkRanges::empty();
1574            let mut best: Option<(Arc<FillSession>, ChunkRanges, u64)> = None;
1575            let mut not_coalesced = false;
1576            if let Some(entry) = map.get(&hash) {
1577                for session in &entry.sessions {
1578                    // Dead sessions (ended or cancelled) may never deliver, so their
1579                    // `covered` neither blocks a fresh pull nor is an attach target.
1580                    // Reading `is_dead` under the map lock pins it against a
1581                    // concurrent last-out lease drop (which fires `cancel()` under
1582                    // this same lock).
1583                    if session.is_dead() {
1584                        continue;
1585                    }
1586                    // A live fill advances only as ITS payer pays: the pull runs one
1587                    // credit window past `served_paid` and parks. A request that
1588                    // starts AHEAD of that frontier would park on it too, and an
1589                    // observer's payments extend a fill's paid prefix only once the
1590                    // prefix reaches the observer's start (`extend_served_from`).
1591                    // With the owner not paying — a header-only open (the CLI's
1592                    // multi-source size probe), or a stalled client — the
1593                    // observer starves until its own stall
1594                    // budget expires. So such a request is not coalesced: it OWNS
1595                    // its own span, and the two fills coexist under the hash. A
1596                    // request at or behind the frontier attaches; its payments
1597                    // extend the shared prefix at once.
1598                    //
1599                    // The cost is duplicate egress on the overlap, and only on the
1600                    // part of it not yet in the store: the owner's pull fills just
1601                    // the gaps the store is missing when it starts, so an overlap
1602                    // the live fill already landed is never fetched again. The
1603                    // part still to land is at most the live fill's credit window
1604                    // plus its in-flight draws, and only attaching could save it,
1605                    // which is what starves. So the rule stays, and a claim that
1606                    // skips an overlapping session is counted once.
1607                    if fetch_start > session.served_paid() {
1608                        if !(&r & &session.covered_ranges()).is_empty() {
1609                            not_coalesced = true;
1610                            tracing::debug!(
1611                                %hash,
1612                                fetch_start,
1613                                served_paid = session.served_paid(),
1614                                "fill not coalesced: request starts ahead of the live fill's paid frontier",
1615                            );
1616                        }
1617                        continue;
1618                    }
1619                    let covered = session.covered_ranges();
1620                    let overlap = &r & &covered;
1621                    if !overlap.is_empty() {
1622                        let span = chunk_span(&overlap);
1623                        if best
1624                            .as_ref()
1625                            .is_none_or(|(_, _, best_span)| span > *best_span)
1626                        {
1627                            best = Some((Arc::clone(session), covered.clone(), span));
1628                        }
1629                    }
1630                    covered_union |= covered;
1631                }
1632            }
1633
1634            if not_coalesced && let Some(m) = &self.metrics {
1635                m.fill_not_coalesced.inc();
1636            }
1637
1638            let attach = &r & &covered_union;
1639            let remainder = &r - &covered_union;
1640
1641            if !attach.is_empty()
1642                && let Some((sibling, sib_covered, _)) = best
1643            {
1644                if remainder.is_empty() {
1645                    // Whole R covered by live pulls → pure ATTACH to the sibling.
1646                    let lease = self.mint_lease(&sibling, hash);
1647                    return FillClaim::Attach {
1648                        session: sibling,
1649                        lease,
1650                    };
1651                }
1652                // Partial overlap. Coalesce only when ONE sibling covers all of the
1653                // overlap AND the bytes it does not cover form ONE contiguous span
1654                // the remainder pull can fetch as a single gap. Otherwise fall
1655                // through to a conservative whole-R owner (no double-pull, no wedge).
1656                let attach_covered_by_sibling = (&attach - &sib_covered).is_empty();
1657                let remainder_vs_sibling = &r - &sib_covered;
1658                if attach_covered_by_sibling
1659                    && let Some((rem_offset, rem_len)) =
1660                        contiguous_byte_span(&remainder_vs_sibling, total)
1661                {
1662                    let owner = make_session();
1663                    owner.set_covered(remainder_vs_sibling);
1664                    self.insert_session(&mut map, hash, &owner);
1665                    let owner_lease = self.mint_lease(&owner, hash);
1666                    let attach_lease = self.mint_lease(&sibling, hash);
1667                    return FillClaim::Mixed {
1668                        owner,
1669                        owner_lease,
1670                        attach: sibling,
1671                        attach_lease,
1672                        remainder_offset: rem_offset,
1673                        remainder_len: rem_len,
1674                    };
1675                }
1676            }
1677        }
1678
1679        // OWNER: no overlap, an empty/unalignable R, or the conservative fallback.
1680        // Scope the session to the WHOLE R (so a later identical claim attaches),
1681        // register + mint the owner lease under the SAME lock.
1682        let owner = make_session();
1683        owner.set_covered(r);
1684        self.insert_session(&mut map, hash, &owner);
1685        let lease = self.mint_lease(&owner, hash);
1686        FillClaim::Owner {
1687            session: owner,
1688            lease,
1689        }
1690    }
1691}
1692
1693#[cfg(test)]
1694#[allow(
1695    clippy::unwrap_used,
1696    clippy::expect_used,
1697    clippy::panic,
1698    clippy::indexing_slicing
1699)] // tests
1700mod tests;
1701
1702#[cfg(test)]
1703#[allow(
1704    clippy::unwrap_used,
1705    clippy::expect_used,
1706    clippy::panic,
1707    clippy::indexing_slicing
1708)] // tests
1709mod fill_registry_tests;