Skip to main content

decdn_client/
streamer.rs

1//! The `Streamer` consumption face (#1848): stream one blob's verified,
2//! contiguous front to a consumer AS IT ARRIVES, paced by how fast the consumer
3//! reads — a fetch-like, single-blob face (no chunk dedup, unlike the
4//! `Downloader`).
5//!
6//! One engine, two faces: the `Streamer` is an OUTPUT + SCHEDULING adapter over
7//! the same paid pull engine, not a second engine. It drives the fetch through
8//! the multi-source core with a [`crate::pacer::WindowPacer`] whose window is the
9//! caller's [`PullConfig::read_ahead_bytes`] and whose downstream frontier is the
10//! CONSUMER'S read cursor. So the pull never runs more than the read-ahead bound
11//! ahead of what the consumer has read — fetch (and pay for) a two-hour movie's
12//! first `read_ahead_bytes` only, for a viewer who stops after five minutes.
13//!
14//! [`Streamer::open`] returns two halves. The [`StreamDrive`] is the fetch
15//! itself; the caller polls it for the whole stream, independently of reads, so
16//! an open paid leg keeps paying and draining while the consumer is slow or
17//! paused (a leg left unpolled would miss its voucher deadlines and fault). The
18//! window, not the reads, bounds how far it runs. [`VerifiedReader`] is the
19//! [`tokio::io::AsyncRead`] the caller drains. Its cursor is clamped to the
20//! store's verified contiguous frontier BY CONSTRUCTION: it only ever reads bytes
21//! the engine has already bao-verified into the store, so a tampered chunk group
22//! fails the pull and surfaces as a read error rather than ever reaching the
23//! consumer.
24
25use std::future::Future;
26use std::io;
27use std::path::Path;
28use std::pin::Pin;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::sync::{Arc, Mutex};
31use std::task::{Context, Poll};
32
33use anyhow::Context as _;
34use bytes::Bytes;
35use tokio::io::{AsyncRead, ReadBuf};
36use tokio::sync::Notify;
37
38use decdn_protocol::Coverage;
39
40use crate::credential::CredentialSlot;
41use crate::driver::{PacingWait, WaitReason};
42use crate::health::PeerHealth;
43use crate::pacer::{DownstreamFrontier, PULL_WINDOW_FLOOR, WindowPacer};
44use crate::recovery::RecoveryGate;
45use crate::scheduler::{AcquireEnv, AcquireTarget, ConsumptionPacing, LaneLease, acquire};
46use crate::sink::BlobCache;
47use crate::source::Funder;
48use crate::source_set::{Holder, SourceProvider, SourceSet};
49use crate::stop::{ProgressClock, StopPolicy};
50use crate::{ClientRangedStore, PoolContext, PoolLedger, PullConfig, RangedStore};
51
52/// The most verified bytes one store read hands the reader. It bounds the
53/// reader's buffer independently of the read-ahead window.
54const MAX_READ_CHUNK: u64 = 1024 * 1024;
55
56/// Shared between the drive future and the [`VerifiedReader`]: the fill store,
57/// the consumer cursor, the drive's outcome, and the wake signals that pace one
58/// against the other.
59struct StreamState {
60    /// The bao-verifying fill store the drive future writes and the reader reads.
61    store: ClientRangedStore,
62    /// Content bytes the CONSUMER has read. It is the downstream frontier the
63    /// [`WindowPacer`] gates the pull against, so the fetch stays within one
64    /// read-ahead window of it.
65    cursor: AtomicU64,
66    /// The caller's size hint: the fetch's first claim. The stream ends at the
67    /// size a leg proves ([`ClientRangedStore::proven`]), whatever this says.
68    hint: u64,
69    /// Woken by the reader when `cursor` advances, so a pull parked on a full
70    /// read-ahead window ([`PaceDecision::Wait`](crate::pacer::PaceDecision::Wait))
71    /// re-decides.
72    consumed: Notify,
73    /// Woken by the drive when the verified frontier may have moved or the drive
74    /// has ended, so a reader waiting for bytes re-reads the store.
75    progressed: Notify,
76    /// The drive's outcome: `None` while it runs, then its result. A drive
77    /// dropped before it finishes records an error, so the reader never waits
78    /// on a fetch that nobody polls.
79    outcome: Mutex<Option<Result<(), Arc<anyhow::Error>>>>,
80}
81
82impl StreamState {
83    /// Record the drive's outcome once (the first record wins) and wake the reader.
84    fn finish(&self, result: anyhow::Result<()>) {
85        if let Ok(mut slot) = self.outcome.lock()
86            && slot.is_none()
87        {
88            *slot = Some(result.map_err(Arc::new));
89        }
90        self.progressed.notify_waiters();
91    }
92
93    /// The drive's outcome, or `None` while it still runs. A poisoned lock reads
94    /// as a failed drive rather than a running one, so the reader cannot hang.
95    fn outcome(&self) -> Option<Result<(), Arc<anyhow::Error>>> {
96        match self.outcome.lock() {
97            Ok(slot) => slot.clone(),
98            Err(_) => Some(Err(Arc::new(anyhow::anyhow!(
99                "stream outcome lock poisoned"
100            )))),
101        }
102    }
103}
104
105/// The [`PacingWait`] the streaming drive parks on when its read-ahead window is
106/// full: it resolves once the consumer's cursor advances past what the `Wait`
107/// observed.
108///
109/// While verified bytes sit unread ahead of the consumer's cursor, the stop
110/// policy's clock holds: the consumer is the bottleneck, and a consumer that
111/// pauses is not a source that stalls. Once the consumer has read everything
112/// present, the missing bytes are the fetch's to deliver, so the clock runs
113/// even while a lane waits here.
114struct ConsumedWait {
115    state: Arc<StreamState>,
116    clock: Arc<ProgressClock>,
117}
118
119impl ConsumedWait {
120    /// Whether verified bytes wait unread ahead of the consumer's `cursor`. A
121    /// store fault reads as nothing unread, so the clock runs.
122    async fn unread_ahead(&self, cursor: u64) -> bool {
123        present_frontier(&self.state.store)
124            .await
125            .is_ok_and(|frontier| frontier > cursor)
126    }
127}
128
129impl PacingWait for ConsumedWait {
130    fn wait(
131        &self,
132        observed: DownstreamFrontier,
133        _reason: WaitReason,
134    ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
135        Box::pin(async move {
136            // A lane parks only between legs, after its last leg's bytes are in
137            // the store. Wake the reader so it hands them out: the cursor can
138            // only advance from bytes the reader has seen.
139            self.state.progressed.notify_waiters();
140            loop {
141                // Register interest BEFORE the checks, so a consume or a landed
142                // byte between the checks and the await cannot be missed.
143                let consumed = self.state.consumed.notified();
144                let landed = self.state.progressed.notified();
145                tokio::pin!(consumed, landed);
146                consumed.as_mut().enable();
147                landed.as_mut().enable();
148                let cursor = self.state.cursor.load(Ordering::SeqCst);
149                if cursor > observed.served_paid {
150                    return;
151                }
152                let _hold = self.unread_ahead(cursor).await.then(|| self.clock.hold());
153                tokio::select! {
154                    () = consumed => {}
155                    () = landed => {}
156                }
157            }
158        })
159    }
160}
161
162/// One provider's paid lane: the paid source plus the `(ctx, ledger)` that
163/// pays it. A [`SourceProvider`] builds one per holder, and both faces fetch
164/// across them.
165///
166/// A lane that faults mid-fetch cools and returns; its remainder continues
167/// from another lane meanwhile, resuming from the store's verified frontier so
168/// no delivered byte is re-pulled or re-paid. Every lane names a distinct
169/// on-chain provider (one voucher stream per `(signer, provider)` lane).
170///
171/// Build one with [`Self::new`], and set [`Self::coverage`] for a partial
172/// holder.
173#[non_exhaustive]
174pub struct StreamCandidate<S> {
175    /// The paid source — one provider's `cdn/client/v1` requester.
176    pub source: S,
177    /// The buyer context paying this candidate's provider (shared behind
178    /// interior mutability so a funding recovery step's deposit is visible to
179    /// its next open).
180    pub ctx: Arc<Mutex<PoolContext>>,
181    /// This candidate's per-`(signer, provider)` voucher ledger.
182    pub ledger: Arc<PoolLedger>,
183    /// This candidate's measured block coverage for the blob being fetched
184    /// (#1506), or `None` to treat it as a full holder. A partial holder MUST
185    /// set this so the scheduler never assigns it — and it never steals — a
186    /// range it does not hold; `None` maps to [`Coverage::full`] sized to the
187    /// blob, the right default for the common case that discovery yields whole-
188    /// blob holders. The bitmap is sized to ONE blob, so a [`crate::Downloader`]
189    /// given several targets applies it to each of them: set it only on a
190    /// single-target fetch.
191    pub coverage: Option<Coverage>,
192    /// What this candidate's lane holds while the fetch runs
193    /// ([`crate::LaneLease`]). With `widen` set, it is released when the
194    /// lane's own worker ends; it is released at the latest when the fetch
195    /// returns. Like `coverage`, set it only on a single-target fetch: the
196    /// first target's fetch releases it.
197    #[doc(hidden)]
198    pub lease: LaneLease,
199    /// How this lane takes a stream beyond its `lease` ([`crate::LaneWiden`]),
200    /// or `None` to stay at one stream: an extra stream for each queued range
201    /// that no idle lane takes, and the stream the lane starts again on once
202    /// it gave its `lease` back. Each granted stream is given back as its
203    /// worker stops, so it serves every target of a [`crate::Downloader`]
204    /// alike.
205    #[doc(hidden)]
206    pub widen: Option<crate::LaneWiden>,
207}
208
209impl<S> StreamCandidate<S> {
210    /// A whole-blob holder's lane: `source` paid from `ctx` through `ledger`,
211    /// holding no lease and staying at one stream.
212    #[must_use]
213    pub fn new(source: S, ctx: Arc<Mutex<PoolContext>>, ledger: Arc<PoolLedger>) -> Self {
214        Self {
215            source,
216            ctx,
217            ledger,
218            coverage: None,
219            lease: LaneLease::default(),
220            widen: None,
221        }
222    }
223}
224
225impl<S> std::fmt::Debug for StreamCandidate<S> {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        f.debug_struct("StreamCandidate").finish_non_exhaustive()
228    }
229}
230
231/// What one stream's drive fetches with: the sources, the payment seams and
232/// the stop, moved into the drive future.
233struct DriveInputs<P, F> {
234    provider: P,
235    holders: Vec<Holder>,
236    health: Arc<PeerHealth>,
237    funder: F,
238    recovery: Arc<RecoveryGate>,
239    credentials: Option<CredentialSlot>,
240    stop: StopPolicy,
241    /// The largest blob accepted, or `0` for no cap.
242    max_blob_bytes: u64,
243}
244
245/// Run the streaming fetch into the store through [`crate::acquire`], bounded
246/// to one read-ahead window ahead of the consumer's cursor and capped to
247/// `lane_cap` concurrent lanes, then (on a clean finish) tee the whole verified
248/// blob to `cache` for revisits.
249///
250/// A source that faults cools and returns, and its remainder moves to another
251/// meanwhile, resuming from the store's verified frontier so no delivered byte
252/// is re-pulled or re-paid.
253async fn run_drive<P, F>(
254    state: Arc<StreamState>,
255    inputs: DriveInputs<P, F>,
256    read_ahead: u64,
257    lane_cap: usize,
258    cache: Arc<dyn BlobCache>,
259    hash: [u8; 32],
260) -> anyhow::Result<()>
261where
262    P: SourceProvider,
263    F: Funder,
264{
265    let DriveInputs {
266        provider,
267        holders,
268        health,
269        funder,
270        recovery,
271        credentials,
272        stop,
273        max_blob_bytes,
274    } = inputs;
275    // Below one pull-window floor (one payment interval plus the chunk-group
276    // roundings, see `PULL_WINDOW_FLOOR`) the window can floor a lane's room to
277    // zero before the consumer has a byte to read, and neither side then moves.
278    let pacer = WindowPacer::new(read_ahead.max(PULL_WINDOW_FLOOR));
279    let on_progress = {
280        let state = Arc::clone(&state);
281        // Each verified-progress report may mean new bytes in the store: wake the
282        // reader to look.
283        move |_position: u64, _total: u64| state.progressed.notify_waiters()
284    };
285    let downstream = {
286        let state = Arc::clone(&state);
287        move || DownstreamFrontier {
288            served_paid: state.cursor.load(Ordering::SeqCst),
289            serve_demand: 0,
290        }
291    };
292    let wait = ConsumedWait {
293        state: Arc::clone(&state),
294        clock: Arc::clone(&stop.clock),
295    };
296    let pacing = ConsumptionPacing {
297        downstream: &downstream,
298        pacing_wait: &wait,
299    };
300    let mut sources = SourceSet::new(&provider, hash, health, holders);
301    let ranges = [(0, state.store.bound())];
302    // Under consumption pacing `acquire` turns its lane watchdog off: a lane
303    // parked on the consumer cursor is waiting, not stalled. A genuinely silent
304    // source trips its own per-stream idle window mid-read instead.
305    let env = AcquireEnv {
306        pacer: &pacer,
307        funder: &funder,
308        recovery: &recovery,
309        credentials: credentials.as_ref(),
310        // Small, bounded front parallelism: a paced stream wants a little
311        // same-region fan-out, not a full download's striping.
312        max_lanes: lane_cap.max(1),
313        stop: &stop,
314        on_progress: Some(&on_progress),
315        ledgers: None,
316        pacing: Some(&pacing),
317        max_blob_bytes,
318    };
319    let result = acquire(
320        AcquireTarget {
321            store: &state.store,
322            hash,
323            total_bytes: state.hint,
324            ranges: &ranges,
325        },
326        &mut sources,
327        &env,
328    )
329    .await;
330    if result.is_ok() {
331        // Tee the whole verified blob, `[0, proven)`, to the cache for a
332        // revisit. Best-effort: a cache write failure never fails the delivered
333        // stream. The store is left at `.partial` (a stream is not kept as a
334        // file), and reading its fully-present content needs no finalize.
335        //
336        // Only when the cache actually stores it: a `NoCache` (the `decdn fetch
337        // -o -` path) reports `caches() == false`, so a huge blob is never read
338        // whole into memory just to be dropped — the stream stays memory-bounded.
339        if cache.caches()
340            && let Some(proven) = state.store.proven()
341            && let Ok(whole) = state.store.read(0, proven).await
342        {
343            let _ = cache.put(hash, 0, whole).await;
344        }
345    }
346    result
347}
348
349/// Stream a single blob's verified front to a consumer, paced by consumption.
350///
351/// Holds the injected pull machinery — the blob's `holders`, the
352/// [`SourceProvider`] that builds their lanes, the command-wide
353/// [`PeerHealth`], and one `funder` — plus a scratch directory for the fill
354/// store. The front is fetched through [`crate::acquire`] across a small,
355/// bounded set of lanes; a lane that faults cools and returns.
356/// [`Streamer::open`] consumes it to start one stream.
357///
358/// Read inside [`StreamDrive::alongside`], so the drive keeps paying and
359/// draining open legs while the reader waits on its consumer. See the `stream`
360/// example for the whole sequence.
361///
362/// ```no_run
363/// use std::path::Path;
364/// use std::sync::Arc;
365///
366/// use decdn_client::source::BlobSource;
367/// use decdn_client::{
368///     Funder, NoCache, ProgressClock, PullConfig, StaticSources, StopPolicy, StreamCandidate,
369///     Streamer,
370/// };
371///
372/// async fn stream<S: BlobSource, F: Funder>(
373///     candidates: Vec<StreamCandidate<S>>,
374///     funder: F,
375///     hash: [u8; 32],
376///     total_bytes: u64,
377///     scratch: &Path,
378/// ) -> anyhow::Result<()> {
379///     let streamer = Streamer::new(StaticSources::new(candidates)?, funder, scratch);
380///     let stop = StopPolicy::new(true, None, Arc::new(ProgressClock::new()));
381///     let (mut reader, mut drive) = streamer
382///         .open(hash, total_bytes, &PullConfig::new(), Arc::new(NoCache), stop)
383///         .await?;
384///     let mut out = tokio::io::stdout();
385///     drive.alongside(tokio::io::copy(&mut reader, &mut out)).await?;
386///     Ok(())
387/// }
388/// ```
389pub struct Streamer<'a, P, F> {
390    provider: P,
391    holders: Vec<Holder>,
392    health: Arc<PeerHealth>,
393    funder: F,
394    recovery: Arc<RecoveryGate>,
395    /// A delegated stream's swappable voucher credential.
396    credentials: Option<CredentialSlot>,
397    /// A scratch directory the fill store's `.partial` lives in for the stream's
398    /// lifetime. The caller owns it (and its cleanup); a streamed blob is not
399    /// kept, so a temporary directory is the usual choice. The `.partial` grows
400    /// to the whole blob as the stream advances, so the directory needs room
401    /// for all of it.
402    scratch: &'a Path,
403    /// The largest blob accepted, in bytes, or `0` for no cap
404    /// ([`Self::max_blob_bytes`]).
405    max_blob_bytes: u64,
406}
407
408impl<P, F> std::fmt::Debug for Streamer<'_, P, F> {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        f.debug_struct("Streamer")
411            .field("holders", &self.holders.len())
412            .field("recovery", &self.recovery)
413            .field("scratch", &self.scratch)
414            .finish_non_exhaustive()
415    }
416}
417
418impl<'a, P, F> Streamer<'a, P, F> {
419    /// Build a streamer whose holders and lanes `provider` finds and builds,
420    /// funded through `funder`, filling into `scratch`.
421    ///
422    /// The stream starts by discovering its holders, under a fresh
423    /// [`RecoveryGate`]. The builder methods change both.
424    #[must_use]
425    pub fn new(provider: P, funder: F, scratch: &'a Path) -> Self {
426        Self {
427            provider,
428            holders: Vec::new(),
429            health: Arc::default(),
430            funder,
431            recovery: Arc::default(),
432            credentials: None,
433            scratch,
434            max_blob_bytes: 0,
435        }
436    }
437
438    /// Start the stream from `holders`, in preference order, instead of
439    /// discovering them. A probe's measured holders go here.
440    #[must_use]
441    pub fn holders(mut self, holders: Vec<Holder>) -> Self {
442        self.holders = holders;
443        self
444    }
445
446    /// Run the funding recovery step under `recovery`, the fetch's gate. A
447    /// caller that has already run part of the fetch under a gate (a first
448    /// open, an earlier pass) passes that gate, so the progress rule spans the
449    /// whole fetch.
450    #[must_use]
451    pub fn recovery(mut self, recovery: Arc<RecoveryGate>) -> Self {
452        self.recovery = recovery;
453        self
454    }
455
456    /// Pay as a delegate under `slot`'s credential, as
457    /// [`crate::Downloader::credentials`] does.
458    #[must_use]
459    pub fn credentials(mut self, slot: CredentialSlot) -> Self {
460        self.credentials = Some(slot);
461        self
462    }
463
464    /// Record source faults into `health`, which a command shares across its
465    /// fetches so a holder that faulted on one stays cooled on the next.
466    #[doc(hidden)]
467    #[must_use]
468    pub fn health(mut self, health: Arc<PeerHealth>) -> Self {
469        self.health = health;
470        self
471    }
472
473    /// Cap the stream at `max_blob_bytes` (`0` for no cap). A size claim above
474    /// the cap is clamped to it before the fill store is sized, and a blob that
475    /// holds bytes past the cap ends the stream with [`crate::BlobTooLarge`]
476    /// ([`crate::AcquireEnv::max_blob_bytes`]).
477    #[must_use]
478    pub const fn max_blob_bytes(mut self, max_blob_bytes: u64) -> Self {
479        self.max_blob_bytes = max_blob_bytes;
480        self
481    }
482}
483
484impl<P, F> Streamer<'_, P, F>
485where
486    P: SourceProvider,
487    F: Funder,
488{
489    /// Open a consumption-paced stream over `hash` (whose content length is
490    /// `total_bytes`), returning the [`VerifiedReader`] the caller drains and the
491    /// [`StreamDrive`] that fetches into it.
492    ///
493    /// The caller MUST poll the drive for as long as it reads — for example with
494    /// [`StreamDrive::alongside`] around its read loop. The drive runs apart from
495    /// the reads so a paid leg keeps paying while the consumer is slow or paused;
496    /// the read-ahead window, not the reads, bounds it. Dropping the drive stops
497    /// the fetch, and the reader then ends with an error after the verified bytes
498    /// it already holds.
499    ///
500    /// The drive borrows `'a` from its sources — a real `PeerSource` over a
501    /// borrowed `Endpoint` is not `'static` — so the caller keeps the endpoint
502    /// (and provider) alive for as long as it polls the drive. The reader owns
503    /// its state and borrows nothing.
504    ///
505    /// `stop` decides when a stream that makes no verified progress gives up;
506    /// the reader then ends with [`crate::GaveUp`]'s message.
507    ///
508    /// On a REVISIT — `cache` already holds the whole blob — the reader serves
509    /// straight from the cache, the drive is already complete, and the network is
510    /// never touched. Otherwise the fetch never runs more than
511    /// `config.read_ahead_bytes` (raised to at least [`PULL_WINDOW_FLOOR`]) ahead
512    /// of the consumer's cursor, and the verified blob is teed to `cache` for the
513    /// next revisit.
514    ///
515    /// `total_bytes` is the first size claim, a hint. A resumed store's bound
516    /// wins, and a leg that verifies the final chunk proves the size. `hash` is
517    /// the bao root every byte is verified against; a wrong hash surfaces as a
518    /// read error, never as silent corruption.
519    ///
520    /// # Errors
521    ///
522    /// A cache read fault at open, or a fill-store open/create I/O error. Faults
523    /// DURING the fetch (a refused or stalled pull, a verification failure)
524    /// surface later, from the reader.
525    pub async fn open<'a>(
526        self,
527        hash: [u8; 32],
528        total_bytes: u64,
529        config: &PullConfig,
530        cache: Arc<dyn BlobCache>,
531        stop: StopPolicy,
532    ) -> anyhow::Result<(VerifiedReader, StreamDrive<'a>)>
533    where
534        P: 'a,
535        F: 'a,
536    {
537        if let Some(cached) = cache.get(hash, 0, total_bytes).await?
538            && u64::try_from(cached.len()).is_ok_and(|len| len == total_bytes)
539        {
540            return Ok((
541                VerifiedReader::Cached {
542                    data: cached,
543                    pos: 0,
544                },
545                StreamDrive {
546                    fetch: None,
547                    state: None,
548                    error: None,
549                },
550            ));
551        }
552
553        let stem = blake3::Hash::from_bytes(hash).to_hex();
554        // A claim above the cap never sizes the store.
555        let total_bytes = if self.max_blob_bytes > 0 {
556            total_bytes.min(self.max_blob_bytes)
557        } else {
558            total_bytes
559        };
560        // Off the runtime: opening a finalized blob hashes its final file.
561        let store = {
562            let (scratch, stem) = (self.scratch.to_path_buf(), stem.to_string());
563            tokio::task::spawn_blocking(move || {
564                ClientRangedStore::open_or_create(&scratch, &stem, hash, total_bytes)
565                    .map_err(|e| anyhow::anyhow!("open stream store for {stem}: {e}"))
566            })
567            .await
568            .map_err(|e| anyhow::anyhow!("open stream store task: {e}"))??
569        };
570        let state = Arc::new(StreamState {
571            store,
572            cursor: AtomicU64::new(0),
573            hint: total_bytes,
574            consumed: Notify::new(),
575            progressed: Notify::new(),
576            outcome: Mutex::new(None),
577        });
578        let fetch = Box::pin(run_drive(
579            Arc::clone(&state),
580            DriveInputs {
581                provider: self.provider,
582                holders: self.holders,
583                health: self.health,
584                funder: self.funder,
585                recovery: self.recovery,
586                credentials: self.credentials,
587                stop,
588                max_blob_bytes: self.max_blob_bytes,
589            },
590            config.read_ahead_bytes,
591            config.streamer_lane_cap,
592            cache,
593            hash,
594        ));
595        Ok((
596            VerifiedReader::Live(LiveReader {
597                state: Arc::clone(&state),
598                read: None,
599                buffered: Bytes::new(),
600            }),
601            StreamDrive {
602                fetch: Some(fetch),
603                state: Some(state),
604                error: None,
605            },
606        ))
607    }
608}
609
610/// The fetch half of an open stream: a [`Future`] that pulls the blob into the
611/// fill store, paced on the reader's cursor, and resolves once the fetch ends.
612/// Its outcome reaches the consumer through the [`VerifiedReader`]: a failure
613/// surfaces as a read error after the verified prefix before it.
614///
615/// Poll it for as long as the reader is read, apart from the reads —
616/// [`Self::alongside`] does exactly that. Dropping it before it resolves stops
617/// the fetch and ends the reader with an error.
618pub struct StreamDrive<'a> {
619    /// The fetch. `None` once it has resolved, and for a cache hit.
620    fetch: Option<Pin<Box<dyn Future<Output = anyhow::Result<()>> + 'a>>>,
621    /// The state the outcome is recorded into. `None` for a cache hit.
622    state: Option<Arc<StreamState>>,
623    /// The typed error the fetch ended with, until [`Self::take_error`] takes
624    /// it. The reader carries only its message.
625    error: Option<anyhow::Error>,
626}
627
628impl std::fmt::Debug for StreamDrive<'_> {
629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630        f.debug_struct("StreamDrive")
631            .field("running", &self.fetch.is_some())
632            .finish_non_exhaustive()
633    }
634}
635
636impl StreamDrive<'_> {
637    /// Run `consume` to completion while this drive runs beside it, and return
638    /// `consume`'s output. The drive is polled first on every wake, so an open
639    /// paid leg is never starved by a slow consumer. If the drive finishes first,
640    /// `consume` keeps running to the end of the stream. If `consume` finishes
641    /// first, the drive stays where it is: call this again to resume it, or drop
642    /// it to stop the fetch.
643    pub async fn alongside<T>(&mut self, consume: impl Future<Output = T>) -> T {
644        tokio::pin!(consume);
645        tokio::select! {
646            biased;
647            () = &mut *self => consume.await,
648            out = &mut consume => out,
649        }
650    }
651
652    /// The error the fetch ended with, typed as the fetch returned it (a
653    /// [`crate::GaveUp`], a fatal fault, a unanimous verdict of the sources),
654    /// once the drive has resolved with one. A reader error that follows a
655    /// failed fetch carries only this error's message, so a caller that needs
656    /// to act on the error's type takes it here.
657    pub const fn take_error(&mut self) -> Option<anyhow::Error> {
658        self.error.take()
659    }
660}
661
662impl Future for StreamDrive<'_> {
663    type Output = ();
664
665    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
666        let this = &mut *self;
667        let Some(fetch) = this.fetch.as_mut() else {
668            return Poll::Ready(());
669        };
670        match fetch.as_mut().poll(cx) {
671            Poll::Ready(result) => {
672                this.fetch = None;
673                let recorded = match result {
674                    Ok(()) => Ok(()),
675                    Err(err) => {
676                        let message = anyhow::anyhow!("{err:#}");
677                        this.error = Some(err);
678                        Err(message)
679                    }
680                };
681                if let Some(state) = &this.state {
682                    state.finish(recorded);
683                }
684                Poll::Ready(())
685            }
686            Poll::Pending => Poll::Pending,
687        }
688    }
689}
690
691impl Drop for StreamDrive<'_> {
692    /// A drive dropped before it resolves records a failure, so a reader still
693    /// waiting on it ends with an error instead of hanging.
694    fn drop(&mut self) {
695        if self.fetch.is_some()
696            && let Some(state) = &self.state
697        {
698            state.finish(Err(anyhow::anyhow!(
699                "stream fetch stopped before the blob was complete"
700            )));
701        }
702    }
703}
704
705/// An [`AsyncRead`] over a blob's verified contiguous front, clamped to the
706/// verified frontier by construction (it only reads bytes the engine has already
707/// bao-verified). Its cursor paces the paired [`StreamDrive`].
708#[non_exhaustive]
709pub enum VerifiedReader {
710    /// A whole-blob cache hit: served straight from memory, no fetch.
711    Cached {
712        /// The cached blob.
713        data: Bytes,
714        /// Bytes already handed to the consumer.
715        pos: usize,
716    },
717    /// A live fetch: the reader waits for the drive to verify bytes into the
718    /// fill store and hands out the verified prefix.
719    Live(LiveReader),
720}
721
722/// One in-flight [`next_verified`] read: the next verified span, or `None` at the
723/// end of the stream.
724type VerifiedRead = Pin<Box<dyn Future<Output = io::Result<Option<Bytes>>> + Send>>;
725
726/// The live half of a [`VerifiedReader`]: it reads the verified prefix out of the
727/// fill store as the paired [`StreamDrive`] lands it, and advances the cursor the
728/// drive paces on.
729pub struct LiveReader {
730    state: Arc<StreamState>,
731    /// An in-flight store read of the next verified span: `Some(bytes)`, or
732    /// `None` at the end of the stream. It waits inside for bytes to land.
733    read: Option<VerifiedRead>,
734    /// Verified bytes read from the store, not yet handed to the consumer.
735    buffered: Bytes,
736}
737
738/// The end of the store's contiguous verified-present run from offset 0 — the
739/// frontier the reader may read up to. Several lanes can fill the store at once,
740/// so the present set may hold later runs too; only the run from offset 0 is
741/// readable in order, and anything else means `0`.
742///
743/// # Errors
744///
745/// A store fault reading its present ranges (I/O, a poisoned lock). Surfaced to
746/// the reader rather than masked as "nothing present", so a real fault does not
747/// look like an empty stream that stalls or ends early.
748async fn present_frontier(store: &ClientRangedStore) -> anyhow::Result<u64> {
749    let present = store
750        .present_ranges()
751        .await
752        .context("read present ranges")?;
753    Ok(
754        match crate::driver::contiguous_byte_ranges(&present, store.bound()).first() {
755            Some(&(0, len)) => len,
756            _ => 0,
757        },
758    )
759}
760
761/// Wait until verified bytes past `cursor` are in the store, then read up to
762/// [`MAX_READ_CHUNK`] of them. `None` is the end of the stream: the cursor has
763/// reached the size a leg proved. A drive failure surfaces only once every
764/// verified byte before it has been read.
765async fn next_verified(state: Arc<StreamState>, cursor: u64) -> io::Result<Option<Bytes>> {
766    loop {
767        if state.store.proven().is_some_and(|proven| cursor >= proven) {
768            return Ok(None);
769        }
770        // Register for both wakeups BEFORE reading the frontier, so a progress
771        // report or a checkpoint between the read and the wait is not lost.
772        let progressed = state.progressed.notified();
773        tokio::pin!(progressed);
774        progressed.as_mut().enable();
775        let grew = state.store.present_grew().notified();
776        tokio::pin!(grew);
777        grew.as_mut().enable();
778        // Read the outcome before the frontier: a drive that ended before this
779        // frontier read left every verified byte in it.
780        let outcome = state.outcome();
781        let frontier = present_frontier(&state.store)
782            .await
783            .map_err(|e| io::Error::other(format!("present frontier: {e:#}")))?;
784        if frontier > cursor {
785            let len = (frontier - cursor).min(MAX_READ_CHUNK);
786            return state.store.read(cursor, len).await.map(Some).map_err(|e| {
787                io::Error::other(format!("read verified prefix: {:#}", anyhow::Error::new(e)))
788            });
789        }
790        match outcome {
791            Some(Err(e)) => {
792                return Err(io::Error::other(format!("stream fetch failed: {e:#}")));
793            }
794            Some(Ok(())) => {
795                return Err(io::Error::other(format!(
796                    "stream fetch finished with {cursor} of {} bytes readable",
797                    state.store.bound()
798                )));
799            }
800            None => {}
801        }
802        // Nothing new yet: wait for the drive or for a checkpoint.
803        tokio::select! {
804            () = progressed => {}
805            () = grew => {}
806        }
807    }
808}
809
810impl std::fmt::Debug for LiveReader {
811    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812        f.debug_struct("LiveReader")
813            .field("cursor", &self.state.cursor.load(Ordering::SeqCst))
814            .field("bound", &self.state.store.bound())
815            .field("buffered", &self.buffered.len())
816            .finish_non_exhaustive()
817    }
818}
819
820impl std::fmt::Debug for VerifiedReader {
821    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
822        match self {
823            Self::Cached { data, pos } => f
824                .debug_struct("VerifiedReader::Cached")
825                .field("len", &data.len())
826                .field("pos", pos)
827                .finish(),
828            Self::Live(_) => f
829                .debug_struct("VerifiedReader::Live")
830                .finish_non_exhaustive(),
831        }
832    }
833}
834
835impl LiveReader {
836    fn poll_read(&mut self, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
837        loop {
838            // 1. Hand out any bytes already read from the store.
839            if !self.buffered.is_empty() {
840                let n = self.buffered.len().min(buf.remaining());
841                if n == 0 {
842                    return Poll::Ready(Ok(()));
843                }
844                // Convert BEFORE handing out any bytes, so a (practically
845                // impossible) failure surfaces as an error rather than
846                // desynchronizing the cursor from bytes already delivered.
847                let Ok(taken) = u64::try_from(n) else {
848                    return Poll::Ready(Err(io::Error::other("read length does not fit u64")));
849                };
850                let chunk = self.buffered.split_to(n);
851                buf.put_slice(&chunk);
852                self.state.cursor.fetch_add(taken, Ordering::SeqCst);
853                // Unpark a pull parked on a full read-ahead window.
854                self.state.consumed.notify_waiters();
855                return Poll::Ready(Ok(()));
856            }
857            // 2. Wait for (or start) the read of the next verified span.
858            let read = self.read.get_or_insert_with(|| {
859                let state = Arc::clone(&self.state);
860                let cursor = state.cursor.load(Ordering::SeqCst);
861                Box::pin(next_verified(state, cursor))
862            });
863            match read.as_mut().poll(cx) {
864                Poll::Ready(Ok(Some(bytes))) => {
865                    self.read = None;
866                    self.buffered = bytes;
867                }
868                Poll::Ready(Ok(None)) => {
869                    self.read = None;
870                    return Poll::Ready(Ok(()));
871                }
872                Poll::Ready(Err(e)) => {
873                    self.read = None;
874                    return Poll::Ready(Err(e));
875                }
876                Poll::Pending => return Poll::Pending,
877            }
878        }
879    }
880}
881
882impl AsyncRead for VerifiedReader {
883    fn poll_read(
884        self: Pin<&mut Self>,
885        cx: &mut Context<'_>,
886        buf: &mut ReadBuf<'_>,
887    ) -> Poll<io::Result<()>> {
888        match self.get_mut() {
889            VerifiedReader::Cached { data, pos } => {
890                let remaining = data.len().saturating_sub(*pos);
891                if remaining == 0 {
892                    return Poll::Ready(Ok(()));
893                }
894                let n = remaining.min(buf.remaining());
895                if let Some(slice) = data.get(*pos..pos.saturating_add(n)) {
896                    buf.put_slice(slice);
897                    *pos = pos.saturating_add(n);
898                }
899                Poll::Ready(Ok(()))
900            }
901            VerifiedReader::Live(live) => live.poll_read(cx, buf),
902        }
903    }
904}
905
906#[cfg(test)]
907mod tests;