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        run: None,
319    };
320    let result = acquire(
321        AcquireTarget {
322            store: &state.store,
323            hash,
324            total_bytes: state.hint,
325            ranges: &ranges,
326        },
327        &mut sources,
328        &env,
329    )
330    .await;
331    if result.is_ok() {
332        // Tee the whole verified blob, `[0, proven)`, to the cache for a
333        // revisit. Best-effort: a cache write failure never fails the delivered
334        // stream. The store is left at `.partial` (a stream is not kept as a
335        // file), and reading its fully-present content needs no finalize.
336        //
337        // Only when the cache actually stores it: a `NoCache` (the
338        // `decdn get decdn:<hash> -o -` path) reports `caches() == false`, so a
339        // huge blob is never read whole into memory just to be dropped — the
340        // stream stays memory-bounded.
341        if cache.caches()
342            && let Some(proven) = state.store.proven()
343            && let Ok(whole) = state.store.read(0, proven).await
344        {
345            let _ = cache.put(hash, 0, whole).await;
346        }
347    }
348    result
349}
350
351/// Stream a single blob's verified front to a consumer, paced by consumption.
352///
353/// Holds the injected pull machinery — the blob's `holders`, the
354/// [`SourceProvider`] that builds their lanes, the command-wide
355/// [`PeerHealth`], and one `funder` — plus a scratch directory for the fill
356/// store. The front is fetched through [`crate::acquire`] across a small,
357/// bounded set of lanes; a lane that faults cools and returns.
358/// [`Streamer::open`] consumes it to start one stream.
359///
360/// Read inside [`StreamDrive::alongside`], so the drive keeps paying and
361/// draining open legs while the reader waits on its consumer. See the `stream`
362/// example for the whole sequence.
363///
364/// ```no_run
365/// use std::path::Path;
366/// use std::sync::Arc;
367///
368/// use decdn_client::source::BlobSource;
369/// use decdn_client::{
370///     Funder, NoCache, ProgressClock, PullConfig, StaticSources, StopPolicy, StreamCandidate,
371///     Streamer,
372/// };
373///
374/// async fn stream<S: BlobSource, F: Funder>(
375///     candidates: Vec<StreamCandidate<S>>,
376///     funder: F,
377///     hash: [u8; 32],
378///     total_bytes: u64,
379///     scratch: &Path,
380/// ) -> anyhow::Result<()> {
381///     let streamer = Streamer::new(StaticSources::new(candidates)?, funder, scratch);
382///     let stop = StopPolicy::new(true, None, Arc::new(ProgressClock::new()));
383///     let (mut reader, mut drive) = streamer
384///         .open(hash, total_bytes, &PullConfig::new(), Arc::new(NoCache), stop)
385///         .await?;
386///     let mut out = tokio::io::stdout();
387///     drive.alongside(tokio::io::copy(&mut reader, &mut out)).await?;
388///     drive.finish().await
389/// }
390/// ```
391pub struct Streamer<'a, P, F> {
392    provider: P,
393    holders: Vec<Holder>,
394    health: Arc<PeerHealth>,
395    funder: F,
396    recovery: Arc<RecoveryGate>,
397    /// A delegated stream's swappable voucher credential.
398    credentials: Option<CredentialSlot>,
399    /// A scratch directory the fill store's `.partial` lives in for the stream's
400    /// lifetime. The caller owns it (and its cleanup); a streamed blob is not
401    /// kept, so a temporary directory is the usual choice. The `.partial` grows
402    /// to the whole blob as the stream advances, so the directory needs room
403    /// for all of it.
404    scratch: &'a Path,
405    /// The largest blob accepted, in bytes, or `0` for no cap
406    /// ([`Self::max_blob_bytes`]).
407    max_blob_bytes: u64,
408}
409
410impl<P, F> std::fmt::Debug for Streamer<'_, P, F> {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        f.debug_struct("Streamer")
413            .field("holders", &self.holders.len())
414            .field("recovery", &self.recovery)
415            .field("scratch", &self.scratch)
416            .finish_non_exhaustive()
417    }
418}
419
420impl<'a, P, F> Streamer<'a, P, F> {
421    /// Build a streamer whose holders and lanes `provider` finds and builds,
422    /// funded through `funder`, filling into `scratch`.
423    ///
424    /// The stream starts by discovering its holders, under a fresh
425    /// [`RecoveryGate`]. The builder methods change both.
426    #[must_use]
427    pub fn new(provider: P, funder: F, scratch: &'a Path) -> Self {
428        Self {
429            provider,
430            holders: Vec::new(),
431            health: Arc::default(),
432            funder,
433            recovery: Arc::default(),
434            credentials: None,
435            scratch,
436            max_blob_bytes: 0,
437        }
438    }
439
440    /// Start the stream from `holders`, in preference order, instead of
441    /// discovering them. A probe's measured holders go here.
442    #[must_use]
443    pub fn holders(mut self, holders: Vec<Holder>) -> Self {
444        self.holders = holders;
445        self
446    }
447
448    /// Run the funding recovery step under `recovery`, the fetch's gate. A
449    /// caller that has already run part of the fetch under a gate (a first
450    /// open, an earlier pass) passes that gate, so the progress rule spans the
451    /// whole fetch.
452    #[must_use]
453    pub fn recovery(mut self, recovery: Arc<RecoveryGate>) -> Self {
454        self.recovery = recovery;
455        self
456    }
457
458    /// Pay as a delegate under `slot`'s credential, as
459    /// [`crate::Downloader::credentials`] does.
460    #[must_use]
461    pub fn credentials(mut self, slot: CredentialSlot) -> Self {
462        self.credentials = Some(slot);
463        self
464    }
465
466    /// Record source faults into `health`, which a command shares across its
467    /// fetches so a holder that faulted on one stays cooled on the next.
468    #[doc(hidden)]
469    #[must_use]
470    pub fn health(mut self, health: Arc<PeerHealth>) -> Self {
471        self.health = health;
472        self
473    }
474
475    /// Cap the stream at `max_blob_bytes` (`0` for no cap). A size claim above
476    /// the cap is clamped to it before the fill store is sized, and a blob that
477    /// holds bytes past the cap ends the stream with [`crate::BlobTooLarge`]
478    /// ([`crate::AcquireEnv::max_blob_bytes`]).
479    #[must_use]
480    pub const fn max_blob_bytes(mut self, max_blob_bytes: u64) -> Self {
481        self.max_blob_bytes = max_blob_bytes;
482        self
483    }
484}
485
486impl<P, F> Streamer<'_, P, F>
487where
488    P: SourceProvider,
489    F: Funder,
490{
491    /// Open a consumption-paced stream over `hash` (whose content length is
492    /// `total_bytes`), returning the [`VerifiedReader`] the caller drains and the
493    /// [`StreamDrive`] that fetches into it.
494    ///
495    /// The caller MUST poll the drive for as long as it reads — for example with
496    /// [`StreamDrive::alongside`] around its read loop. The drive runs apart from
497    /// the reads so a paid leg keeps paying while the consumer is slow or paused;
498    /// the read-ahead window, not the reads, bounds it. Dropping the drive stops
499    /// the fetch, and the reader then ends with an error after the verified bytes
500    /// it already holds.
501    ///
502    /// The drive borrows `'a` from its sources — a real `PeerSource` over a
503    /// borrowed `Endpoint` is not `'static` — so the caller keeps the endpoint
504    /// (and provider) alive for as long as it polls the drive. The reader owns
505    /// its state and borrows nothing.
506    ///
507    /// `stop` decides when a stream that makes no verified progress gives up;
508    /// the reader then ends with [`crate::GaveUp`]'s message.
509    ///
510    /// On a REVISIT — `cache` already holds the whole blob — the reader serves
511    /// straight from the cache, the drive is already complete, and the network is
512    /// never touched. Otherwise the fetch never runs more than
513    /// `config.read_ahead_bytes` (raised to at least [`PULL_WINDOW_FLOOR`]) ahead
514    /// of the consumer's cursor, and the verified blob is teed to `cache` for the
515    /// next revisit.
516    ///
517    /// `total_bytes` is the first size claim, a hint. A resumed store's bound
518    /// wins, and a leg that verifies the final chunk proves the size. `hash` is
519    /// the bao root every byte is verified against; a wrong hash surfaces as a
520    /// read error, never as silent corruption.
521    ///
522    /// # Errors
523    ///
524    /// A cache read fault at open, or a fill-store open/create I/O error. Faults
525    /// DURING the fetch (a refused or stalled pull, a verification failure)
526    /// surface later, from the reader.
527    pub async fn open<'a>(
528        self,
529        hash: [u8; 32],
530        total_bytes: u64,
531        config: &PullConfig,
532        cache: Arc<dyn BlobCache>,
533        stop: StopPolicy,
534    ) -> anyhow::Result<(VerifiedReader, StreamDrive<'a>)>
535    where
536        P: 'a,
537        F: 'a,
538    {
539        if let Some(cached) = cache.get(hash, 0, total_bytes).await?
540            && u64::try_from(cached.len()).is_ok_and(|len| len == total_bytes)
541        {
542            return Ok((
543                VerifiedReader::Cached {
544                    data: cached,
545                    pos: 0,
546                },
547                StreamDrive {
548                    fetch: None,
549                    state: None,
550                    error: None,
551                },
552            ));
553        }
554
555        let stem = blake3::Hash::from_bytes(hash).to_hex();
556        // A claim above the cap never sizes the store.
557        let total_bytes = if self.max_blob_bytes > 0 {
558            total_bytes.min(self.max_blob_bytes)
559        } else {
560            total_bytes
561        };
562        // Off the runtime: opening a finalized blob hashes its final file.
563        let store = {
564            let (scratch, stem) = (self.scratch.to_path_buf(), stem.to_string());
565            tokio::task::spawn_blocking(move || {
566                ClientRangedStore::open_or_create(&scratch, &stem, hash, total_bytes)
567                    .map_err(|e| anyhow::anyhow!("open stream store for {stem}: {e}"))
568            })
569            .await
570            .map_err(|e| anyhow::anyhow!("open stream store task: {e}"))??
571        };
572        let state = Arc::new(StreamState {
573            store,
574            cursor: AtomicU64::new(0),
575            hint: total_bytes,
576            consumed: Notify::new(),
577            progressed: Notify::new(),
578            outcome: Mutex::new(None),
579        });
580        let fetch = Box::pin(run_drive(
581            Arc::clone(&state),
582            DriveInputs {
583                provider: self.provider,
584                holders: self.holders,
585                health: self.health,
586                funder: self.funder,
587                recovery: self.recovery,
588                credentials: self.credentials,
589                stop,
590                max_blob_bytes: self.max_blob_bytes,
591            },
592            config.read_ahead_bytes,
593            config.streamer_lane_cap,
594            cache,
595            hash,
596        ));
597        Ok((
598            VerifiedReader::Live(LiveReader {
599                state: Arc::clone(&state),
600                read: None,
601                buffered: Bytes::new(),
602            }),
603            StreamDrive {
604                fetch: Some(fetch),
605                state: Some(state),
606                error: None,
607            },
608        ))
609    }
610}
611
612/// The fetch half of an open stream: a [`Future`] that pulls the blob into the
613/// fill store, paced on the reader's cursor, and resolves once the fetch ends.
614/// Its outcome reaches the consumer through the [`VerifiedReader`]: a failure
615/// surfaces as a read error after the verified prefix before it.
616///
617/// Poll it for as long as the reader is read, apart from the reads —
618/// [`Self::alongside`] does exactly that. Dropping it before it resolves stops
619/// the fetch and ends the reader with an error.
620pub struct StreamDrive<'a> {
621    /// The fetch. `None` once it has resolved, and for a cache hit.
622    fetch: Option<Pin<Box<dyn Future<Output = anyhow::Result<()>> + 'a>>>,
623    /// The state the outcome is recorded into. `None` for a cache hit.
624    state: Option<Arc<StreamState>>,
625    /// The typed error the fetch ended with, until [`Self::take_error`] takes
626    /// it. The reader carries only its message.
627    error: Option<anyhow::Error>,
628}
629
630impl std::fmt::Debug for StreamDrive<'_> {
631    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
632        f.debug_struct("StreamDrive")
633            .field("running", &self.fetch.is_some())
634            .finish_non_exhaustive()
635    }
636}
637
638impl StreamDrive<'_> {
639    /// Run `consume` to completion while this drive runs beside it, and return
640    /// `consume`'s output. The drive is polled first on every wake, so an open
641    /// paid leg is never starved by a slow consumer. If the drive finishes first,
642    /// `consume` keeps running to the end of the stream. If `consume` finishes
643    /// first, the drive stays where it is: call this again to resume it,
644    /// [`Self::finish`] it, or drop it to stop the fetch.
645    pub async fn alongside<T>(&mut self, consume: impl Future<Output = T>) -> T {
646        tokio::pin!(consume);
647        tokio::select! {
648            biased;
649            () = &mut *self => consume.await,
650            out = &mut consume => out,
651        }
652    }
653
654    /// The error the fetch ended with, typed as the fetch returned it (a
655    /// [`crate::GaveUp`], a fatal fault, a unanimous verdict of the sources),
656    /// once the drive has resolved with one. A reader error that follows a
657    /// failed fetch carries only this error's message, so a caller that needs
658    /// to act on the error's type takes it here.
659    pub const fn take_error(&mut self) -> Option<anyhow::Error> {
660        self.error.take()
661    }
662
663    /// Run the drive to its end and return the error the fetch ended with. A
664    /// consumer that read to the end of the stream calls this before it reports
665    /// success: after the last verified byte, the fetch still awaits its lane
666    /// builds in flight (for at most 30 s), and one that may have escrowed USDC
667    /// no record credits ends the fetch with an error. The reader's end of
668    /// stream does not carry that error.
669    ///
670    /// # Errors
671    ///
672    /// The error the fetch ended with, typed as [`Self::take_error`] returns
673    /// it, unless an earlier [`Self::take_error`] took it.
674    pub async fn finish(&mut self) -> anyhow::Result<()> {
675        (&mut *self).await;
676        self.take_error().map_or(Ok(()), Err)
677    }
678}
679
680impl Future for StreamDrive<'_> {
681    type Output = ();
682
683    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
684        let this = &mut *self;
685        let Some(fetch) = this.fetch.as_mut() else {
686            return Poll::Ready(());
687        };
688        match fetch.as_mut().poll(cx) {
689            Poll::Ready(result) => {
690                this.fetch = None;
691                let recorded = match result {
692                    Ok(()) => Ok(()),
693                    Err(err) => {
694                        let message = anyhow::anyhow!("{err:#}");
695                        this.error = Some(err);
696                        Err(message)
697                    }
698                };
699                if let Some(state) = &this.state {
700                    state.finish(recorded);
701                }
702                Poll::Ready(())
703            }
704            Poll::Pending => Poll::Pending,
705        }
706    }
707}
708
709impl Drop for StreamDrive<'_> {
710    /// A drive dropped before it resolves records a failure, so a reader still
711    /// waiting on it ends with an error instead of hanging.
712    fn drop(&mut self) {
713        if self.fetch.is_some()
714            && let Some(state) = &self.state
715        {
716            state.finish(Err(anyhow::anyhow!(
717                "stream fetch stopped before the blob was complete"
718            )));
719        }
720    }
721}
722
723/// An [`AsyncRead`] over a blob's verified contiguous front, clamped to the
724/// verified frontier by construction (it only reads bytes the engine has already
725/// bao-verified). Its cursor paces the paired [`StreamDrive`].
726#[non_exhaustive]
727pub enum VerifiedReader {
728    /// A whole-blob cache hit: served straight from memory, no fetch.
729    Cached {
730        /// The cached blob.
731        data: Bytes,
732        /// Bytes already handed to the consumer.
733        pos: usize,
734    },
735    /// A live fetch: the reader waits for the drive to verify bytes into the
736    /// fill store and hands out the verified prefix.
737    Live(LiveReader),
738}
739
740/// One in-flight [`next_verified`] read: the next verified span, or `None` at the
741/// end of the stream.
742type VerifiedRead = Pin<Box<dyn Future<Output = io::Result<Option<Bytes>>> + Send>>;
743
744/// The live half of a [`VerifiedReader`]: it reads the verified prefix out of the
745/// fill store as the paired [`StreamDrive`] lands it, and advances the cursor the
746/// drive paces on.
747pub struct LiveReader {
748    state: Arc<StreamState>,
749    /// An in-flight store read of the next verified span: `Some(bytes)`, or
750    /// `None` at the end of the stream. It waits inside for bytes to land.
751    read: Option<VerifiedRead>,
752    /// Verified bytes read from the store, not yet handed to the consumer.
753    buffered: Bytes,
754}
755
756/// The end of the store's contiguous verified-present run from offset 0 — the
757/// frontier the reader may read up to. Several lanes can fill the store at once,
758/// so the present set may hold later runs too; only the run from offset 0 is
759/// readable in order, and anything else means `0`.
760///
761/// # Errors
762///
763/// A store fault reading its present ranges (I/O, a poisoned lock). Surfaced to
764/// the reader rather than masked as "nothing present", so a real fault does not
765/// look like an empty stream that stalls or ends early.
766async fn present_frontier(store: &ClientRangedStore) -> anyhow::Result<u64> {
767    let present = store
768        .present_ranges()
769        .await
770        .context("read present ranges")?;
771    Ok(
772        match crate::driver::contiguous_byte_ranges(&present, store.bound()).first() {
773            Some(&(0, len)) => len,
774            _ => 0,
775        },
776    )
777}
778
779/// Wait until verified bytes past `cursor` are in the store, then read up to
780/// [`MAX_READ_CHUNK`] of them. `None` is the end of the stream: the cursor has
781/// reached the size a leg proved. A drive failure surfaces only once every
782/// verified byte before it has been read.
783async fn next_verified(state: Arc<StreamState>, cursor: u64) -> io::Result<Option<Bytes>> {
784    loop {
785        if state.store.proven().is_some_and(|proven| cursor >= proven) {
786            return Ok(None);
787        }
788        // Register for both wakeups BEFORE reading the frontier, so a progress
789        // report or a checkpoint between the read and the wait is not lost.
790        let progressed = state.progressed.notified();
791        tokio::pin!(progressed);
792        progressed.as_mut().enable();
793        let grew = state.store.present_grew().notified();
794        tokio::pin!(grew);
795        grew.as_mut().enable();
796        // Read the outcome before the frontier: a drive that ended before this
797        // frontier read left every verified byte in it.
798        let outcome = state.outcome();
799        let frontier = present_frontier(&state.store)
800            .await
801            .map_err(|e| io::Error::other(format!("present frontier: {e:#}")))?;
802        if frontier > cursor {
803            let len = (frontier - cursor).min(MAX_READ_CHUNK);
804            return state.store.read(cursor, len).await.map(Some).map_err(|e| {
805                io::Error::other(format!("read verified prefix: {:#}", anyhow::Error::new(e)))
806            });
807        }
808        match outcome {
809            Some(Err(e)) => {
810                return Err(io::Error::other(format!("stream fetch failed: {e:#}")));
811            }
812            Some(Ok(())) => {
813                return Err(io::Error::other(format!(
814                    "stream fetch finished with {cursor} of {} bytes readable",
815                    state.store.bound()
816                )));
817            }
818            None => {}
819        }
820        // Nothing new yet: wait for the drive or for a checkpoint.
821        tokio::select! {
822            () = progressed => {}
823            () = grew => {}
824        }
825    }
826}
827
828impl std::fmt::Debug for LiveReader {
829    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
830        f.debug_struct("LiveReader")
831            .field("cursor", &self.state.cursor.load(Ordering::SeqCst))
832            .field("bound", &self.state.store.bound())
833            .field("buffered", &self.buffered.len())
834            .finish_non_exhaustive()
835    }
836}
837
838impl std::fmt::Debug for VerifiedReader {
839    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840        match self {
841            Self::Cached { data, pos } => f
842                .debug_struct("VerifiedReader::Cached")
843                .field("len", &data.len())
844                .field("pos", pos)
845                .finish(),
846            Self::Live(_) => f
847                .debug_struct("VerifiedReader::Live")
848                .finish_non_exhaustive(),
849        }
850    }
851}
852
853impl LiveReader {
854    fn poll_read(&mut self, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
855        loop {
856            // 1. Hand out any bytes already read from the store.
857            if !self.buffered.is_empty() {
858                let n = self.buffered.len().min(buf.remaining());
859                if n == 0 {
860                    return Poll::Ready(Ok(()));
861                }
862                // Convert BEFORE handing out any bytes, so a (practically
863                // impossible) failure surfaces as an error rather than
864                // desynchronizing the cursor from bytes already delivered.
865                let Ok(taken) = u64::try_from(n) else {
866                    return Poll::Ready(Err(io::Error::other("read length does not fit u64")));
867                };
868                let chunk = self.buffered.split_to(n);
869                buf.put_slice(&chunk);
870                self.state.cursor.fetch_add(taken, Ordering::SeqCst);
871                // Unpark a pull parked on a full read-ahead window.
872                self.state.consumed.notify_waiters();
873                return Poll::Ready(Ok(()));
874            }
875            // 2. Wait for (or start) the read of the next verified span.
876            let read = self.read.get_or_insert_with(|| {
877                let state = Arc::clone(&self.state);
878                let cursor = state.cursor.load(Ordering::SeqCst);
879                Box::pin(next_verified(state, cursor))
880            });
881            match read.as_mut().poll(cx) {
882                Poll::Ready(Ok(Some(bytes))) => {
883                    self.read = None;
884                    self.buffered = bytes;
885                }
886                Poll::Ready(Ok(None)) => {
887                    self.read = None;
888                    return Poll::Ready(Ok(()));
889                }
890                Poll::Ready(Err(e)) => {
891                    self.read = None;
892                    return Poll::Ready(Err(e));
893                }
894                Poll::Pending => return Poll::Pending,
895            }
896        }
897    }
898}
899
900impl AsyncRead for VerifiedReader {
901    fn poll_read(
902        self: Pin<&mut Self>,
903        cx: &mut Context<'_>,
904        buf: &mut ReadBuf<'_>,
905    ) -> Poll<io::Result<()>> {
906        match self.get_mut() {
907            VerifiedReader::Cached { data, pos } => {
908                let remaining = data.len().saturating_sub(*pos);
909                if remaining == 0 {
910                    return Poll::Ready(Ok(()));
911                }
912                let n = remaining.min(buf.remaining());
913                if let Some(slice) = data.get(*pos..pos.saturating_add(n)) {
914                    buf.put_slice(slice);
915                    *pos = pos.saturating_add(n);
916                }
917                Poll::Ready(Ok(()))
918            }
919            VerifiedReader::Live(live) => live.poll_read(cx, buf),
920        }
921    }
922}
923
924#[cfg(test)]
925mod tests;