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;