Skip to main content

primitives/correlated_randomness/stream/buffered/
mod.rs

1pub mod config;
2pub mod messages;
3
4use std::{
5    cell::Cell,
6    collections::VecDeque,
7    marker::PhantomData,
8    sync::{
9        atomic::{AtomicU64, Ordering},
10        Arc,
11    },
12};
13
14pub use config::{Buffer, BufferConfig, SharedBufferConfig};
15use log::{debug, error, info};
16pub use messages::PrefetchHandle;
17use parking_lot::RwLock;
18use tokio::{
19    sync::{
20        mpsc::{self, UnboundedReceiver, UnboundedSender},
21        oneshot,
22    },
23    task::JoinHandle,
24};
25
26use crate::correlated_randomness::{
27    generator::CorrelationGenerator,
28    stream::{
29        buffered::{config::try_read_config, messages::Command},
30        errors::CorrelatedStreamError,
31        futures::Next,
32        CorrelatedStream,
33        NextVec,
34        ResyncHandle,
35    },
36    CorrelatedBatch,
37};
38
39/// A unit of background work sent from the dispatcher to the generator task.
40enum Work {
41    /// Generate at least `n` elements and return them.
42    Generate(usize),
43    /// Advance the generator's logical position by `n` without materializing the elements.
44    Skip(usize),
45}
46
47/// Buffering over a correlation generator, providing both demand-driven streaming
48/// and proactive prefetching interfaces. The buffer is refilled in the background by a
49/// dispatcher/generator task pair; `capacity` bounds aggregate outstanding demand and
50/// `refill_threshold` drives proactive top-ups.
51pub struct BufferedStream<PB: CorrelatedBatch, E> {
52    command_sender: UnboundedSender<Command<PB, E>>,
53    _unsync_marker: PhantomData<Cell<()>>,
54    config: Arc<RwLock<BufferConfig>>,
55    /// Logical position: cumulative items delivered through `next_n`. Written by the dispatcher
56    /// (the single source of truth) and read synchronously here. See
57    /// [`CorrelatedStream::position`].
58    position: Arc<AtomicU64>,
59    dispatcher_handle: JoinHandle<()>,
60    generator_handle: JoinHandle<()>,
61}
62
63impl<PB: CorrelatedBatch, E> Buffer for BufferedStream<PB, E> {
64    fn config(&self) -> &Arc<RwLock<BufferConfig>> {
65        &self.config
66    }
67}
68
69impl<
70        PB: CorrelatedBatch,
71        E: From<CorrelatedStreamError> + Clone + Send + std::fmt::Debug + 'static,
72    > BufferedStream<PB, E>
73{
74    /// Creates a new stream.
75    pub fn new<G: CorrelationGenerator<PB> + Send + 'static>(
76        generator: G,
77        net: G::Net,
78        config: BufferConfig,
79    ) -> Self
80    where
81        E: From<G::Error>,
82    {
83        Self::new_with_shared_config(generator, net, Arc::new(RwLock::new(config)))
84    }
85
86    /// Creates a purely on-demand stream: no proactive refill and effectively unbounded admission,
87    /// so every `next_n` triggers generation of exactly the shortfall (any surplus is buffered).
88    /// Suited to streams whose generator is itself the buffer/limiter (e.g. a dealer client).
89    pub fn new_on_demand<G: CorrelationGenerator<PB> + Send + 'static>(
90        generator: G,
91        net: G::Net,
92    ) -> Self
93    where
94        E: From<G::Error>,
95    {
96        Self::new(
97            generator,
98            net,
99            BufferConfig::lazy(BufferConfig::UNBOUNDED, 0),
100        )
101    }
102
103    /// Creates a new stream.
104    pub fn new_with_shared_config<G: CorrelationGenerator<PB> + Send + 'static>(
105        generator: G,
106        net: G::Net,
107        config: Arc<RwLock<BufferConfig>>,
108    ) -> Self
109    where
110        E: From<G::Error>,
111    {
112        let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<Command<PB, E>>();
113        let (work_tx, work_rx) = mpsc::unbounded_channel::<Work>();
114        let (items_tx, items_rx) = mpsc::unbounded_channel::<Result<Vec<PB::Item>, E>>();
115        let (skip_tx, skip_rx) = mpsc::unbounded_channel::<Result<(), E>>();
116        let position = Arc::new(AtomicU64::new(0));
117        let generator_handle =
118            tokio::spawn(generator_loop(generator, net, work_rx, items_tx, skip_tx));
119        let dispatcher_handle = tokio::spawn(dispatcher_loop(
120            cmd_rx,
121            work_tx,
122            items_rx,
123            skip_rx,
124            config.clone(),
125            position.clone(),
126            G::SUPPORTS_UNILATERAL_SKIP,
127        ));
128        Self {
129            command_sender: cmd_tx,
130            _unsync_marker: PhantomData,
131            config,
132            position,
133            dispatcher_handle,
134            generator_handle,
135        }
136    }
137
138    /// Shuts down the buffer gracefully and waits for all background tasks to exit.
139    ///
140    /// Closing the command channel causes the dispatcher to exit, which in turn drops the work
141    /// channel, causing the generator to exit.  Both tasks are awaited before returning.
142    pub async fn stop(self) {
143        let Self {
144            command_sender,
145            dispatcher_handle,
146            generator_handle,
147            ..
148        } = self;
149        drop(command_sender);
150        let _ = dispatcher_handle.await;
151        let _ = generator_handle.await;
152    }
153}
154
155// ============================
156// ===== Generator Task =======
157// ============================
158
159async fn generator_loop<
160    PB: CorrelatedBatch,
161    G: CorrelationGenerator<PB> + Send,
162    E: From<G::Error> + Send,
163>(
164    mut generator: G,
165    mut net: G::Net,
166    mut work_rx: UnboundedReceiver<Work>,
167    items_tx: UnboundedSender<Result<Vec<PB::Item>, E>>,
168    skip_tx: UnboundedSender<Result<(), E>>,
169) {
170    let log_prefix = format!("<Generator<{}>>", std::any::type_name::<PB>());
171    while let Some(work) = work_rx.recv().await {
172        match work {
173            Work::Generate(n) => {
174                debug!("{log_prefix} generating {n} elements");
175                let result = generator.run_for(n, &mut net).await.map_err(E::from);
176                let stop = result.is_err();
177                // Stop on either a generator error or a dropped dispatcher.
178                if items_tx.send(result).is_err() || stop {
179                    break;
180                }
181            }
182            Work::Skip(n) => {
183                debug!("{log_prefix} skipping {n} elements");
184                let result = generator.skip(n, &mut net).await.map_err(E::from);
185                let stop = result.is_err();
186                if skip_tx.send(result).is_err() || stop {
187                    break;
188                }
189            }
190        }
191    }
192    info!("{log_prefix} exiting");
193}
194
195// ============================
196// ===== Dispatcher Task ======
197// ============================
198
199type ItemsCollected<PB> = Vec<<PB as IntoIterator>::Item>;
200type TotalNeeded = usize;
201type BatchSender<PB, E> = oneshot::Sender<Result<Vec<<PB as IntoIterator>::Item>, E>>;
202
203/// Dispatches a pending resync skip to the generator once no other work is in flight.
204fn maybe_skip(
205    pending_skip: &mut usize,
206    work_in_flight: &mut bool,
207    work_tx: &UnboundedSender<Work>,
208    log_prefix: &str,
209) {
210    if *pending_skip > 0 && !*work_in_flight {
211        debug!("{log_prefix} requesting skip of {} elements", *pending_skip);
212        *work_in_flight = true;
213        let _ = work_tx.send(Work::Skip(*pending_skip));
214        *pending_skip = 0;
215    }
216}
217
218/// Issues a generation order if there is unmet demand and nothing else is running (a pending resync
219/// skip takes priority). Demand = `pending_batches` shortfall + `max(refill top-up, prefetch
220/// deficit)`. `Err` means the config lock timed out and the dispatcher should stop.
221#[allow(clippy::too_many_arguments)]
222fn maybe_generate<PB: CorrelatedBatch, E: From<CorrelatedStreamError>>(
223    work_in_flight: &mut bool,
224    pending_resync: &Option<(usize, oneshot::Sender<Result<(), E>>)>,
225    pending_skip: usize,
226    config: &Arc<RwLock<BufferConfig>>,
227    pending_batches: &VecDeque<(ItemsCollected<PB>, TotalNeeded, BatchSender<PB, E>)>,
228    buffer_len: usize,
229    prefetch_demand: usize,
230    work_tx: &UnboundedSender<Work>,
231    log_prefix: &str,
232) -> Result<(), E> {
233    if *work_in_flight || pending_resync.is_some() || pending_skip != 0 {
234        return Ok(());
235    }
236    let batch_shortfall: usize = pending_batches
237        .iter()
238        .map(|(collected, needed, _)| needed.saturating_sub(collected.len() + buffer_len))
239        .sum();
240    let buf_after = buffer_len.saturating_sub(batch_shortfall);
241    let need = batch_shortfall
242        + try_read_config(config)?
243            .refill_threshold()
244            .saturating_sub(buf_after)
245            .max(prefetch_demand);
246    if need > 0 {
247        debug!("{log_prefix} requesting generation of {need} items");
248        *work_in_flight = true;
249        let _ = work_tx.send(Work::Generate(need));
250    }
251    Ok(())
252}
253
254async fn dispatcher_loop<
255    PB: CorrelatedBatch,
256    E: From<CorrelatedStreamError> + Clone + Send + std::fmt::Debug,
257>(
258    mut cmd_rx: UnboundedReceiver<Command<PB, E>>,
259    work_tx: UnboundedSender<Work>,
260    mut items_rx: UnboundedReceiver<Result<Vec<PB::Item>, E>>,
261    mut skip_rx: UnboundedReceiver<Result<(), E>>,
262    config: Arc<RwLock<BufferConfig>>,
263    shared_position: Arc<AtomicU64>,
264    supports_skip: bool,
265) {
266    let log_prefix = format!("<Dispatcher<{}>>", std::any::type_name::<PB>());
267
268    // Initial capacity is just a hint (the buffer auto-grows on push), clamped so we never
269    // pre-allocate a `BufferConfig::UNBOUNDED` capacity (e.g. from `new_on_demand`).
270    let initial_cap = try_read_config(&config)
271        .map(|c| c.capacity())
272        .unwrap_or(0)
273        .min(1 << 12);
274    let mut buffer: VecDeque<PB::Item> = VecDeque::with_capacity(initial_cap);
275    // Newly generated items still owed to outstanding prefetches.
276    let mut prefetch_demand: usize = 0;
277    // (remaining deficit of newly generated items, completion sender) per outstanding prefetch.
278    let mut prefetch_completions: VecDeque<(usize, oneshot::Sender<Result<(), E>>)> =
279        VecDeque::new();
280    // A single unit of background work (generation or skip) may be outstanding at a time.
281    let mut work_in_flight = false;
282    // Outstanding `next_n` batch requests in FIFO order: (items collected, total needed, sender).
283    let mut pending_batches: VecDeque<(ItemsCollected<PB>, TotalNeeded, BatchSender<PB, E>)> =
284        VecDeque::new();
285    // The logical position lives solely in `shared_position` (the dispatcher is its only writer):
286    // advanced with `fetch_add`, read back with `load` where needed.
287    // An in-flight resync awaiting generator skip completion: (deficit being skipped, completion).
288    // The deficit (not an absolute target) is added to `position` on completion so concurrent
289    // `next_n` increments are preserved.
290    let mut pending_resync: Option<(usize, oneshot::Sender<Result<(), E>>)> = None;
291    // Remaining elements to skip at the generator for the in-flight resync (not yet dispatched).
292    let mut pending_skip: usize = 0;
293    // If a config lock acquisition times out, we propagate this error to all consumers.
294    let mut shutdown_err: Option<E> = None;
295
296    // Lock the config with timeout; on failure, record the error and `break` the outer loop.
297    macro_rules! lock_cfg {
298        () => {
299            match try_read_config(&config) {
300                Ok(g) => g,
301                Err(e) => {
302                    error!("{log_prefix} config lock timeout, shutting down");
303                    shutdown_err = Some(e.into());
304                    break;
305                }
306            }
307        };
308    }
309
310    // Canonical unmet demand: batch shortfalls + prefetch deficits, net of buffered items.
311    macro_rules! outstanding_demand {
312        () => {{
313            let batch_shortfall: usize = pending_batches
314                .iter()
315                .map(|(collected, needed, _)| needed.saturating_sub(collected.len()))
316                .sum();
317            (batch_shortfall + prefetch_demand).saturating_sub(buffer.len())
318        }};
319    }
320
321    loop {
322        tokio::select! {
323            cmd = cmd_rx.recv() => {
324                let Some(cmd) = cmd else {
325                    info!("{log_prefix} command channel closed, shutting down");
326                    break;
327                };
328                match cmd {
329                    Command::RequestN { n_elements, completion } => {
330                        debug!("{log_prefix} batch request for {n_elements} items");
331                        let cap = lock_cfg!().capacity();
332                        if outstanding_demand!() + n_elements > cap {
333                            // Rejected: nothing is delivered, so the position does not advance.
334                            let _ = completion.send(Err(CorrelatedStreamError::RateLimitExceeded.into()));
335                        } else if buffer.len() >= n_elements {
336                            // Fully served from buffer — complete immediately.
337                            let items: Vec<_> = buffer.drain(..n_elements).collect();
338                            shared_position.fetch_add(n_elements as u64, Ordering::Release);
339                            let _ = completion.send(Ok(items));
340                        } else {
341                            // Partially served; need generation for the remainder. The request is
342                            // admitted (committed to deliver in FIFO order), so advance now.
343                            let collected: Vec<_> = buffer.drain(..).collect();
344                            shared_position.fetch_add(n_elements as u64, Ordering::Release);
345                            pending_batches.push_back((collected, n_elements, completion));
346                        }
347                    }
348                    Command::Resync { target, completion } => {
349                        // Snapshot the single source of truth for this arm's checks.
350                        let position = shared_position.load(Ordering::Relaxed);
351                        debug!("{log_prefix} resync to {target} (position {position})");
352                        if pending_resync.is_some() {
353                            // Reject overlapping resyncs: queueing them would only widen the
354                            // window for another desync. The caller must serialize resyncs.
355                            let _ = completion.send(Err(CorrelatedStreamError::ResyncInProgress.into()));
356                        } else if target < position {
357                            let _ = completion.send(Err(CorrelatedStreamError::ResyncRewind {
358                                current: position,
359                                target,
360                            }
361                            .into()));
362                        } else {
363                            // Discard already-buffered elements first (free, local).
364                            let skip = (target - position) as usize;
365                            let drained = skip.min(buffer.len());
366                            buffer.drain(..drained);
367                            shared_position.fetch_add(drained as u64, Ordering::Release);
368                            let deficit = skip - drained;
369                            if deficit == 0 {
370                                let _ = completion.send(Ok(()));
371                            } else if !supports_skip {
372                                // By the lockstep argument, an interactive generator should always
373                                // have the target buffered; a deficit signals a generation desync.
374                                let _ = completion.send(Err(CorrelatedStreamError::ResyncUnsupported {
375                                    generated: position + drained as u64,
376                                    target,
377                                }
378                                .into()));
379                            } else {
380                                // Skip the remainder at the generator, then finalize on completion.
381                                // We stash the deficit (not `target`) so the completion advances
382                                // position relatively, preserving any concurrent next_n increments.
383                                pending_resync = Some((deficit, completion));
384                                pending_skip = deficit;
385                                maybe_skip(&mut pending_skip, &mut work_in_flight, &work_tx, &log_prefix);
386                            }
387                        }
388                    }
389                    Command::Prefetch { n_elements, completion } => {
390                        debug!("{log_prefix} prefetch {n_elements} items");
391                        if buffer.len() >= n_elements {
392                            // Buffer already covers the demand (includes n_elements == 0).
393                            let _ = completion.send(Ok(()));
394                        } else {
395                            let cap = lock_cfg!().capacity();
396                            if outstanding_demand!() + n_elements > cap {
397                                let _ = completion.send(Err(CorrelatedStreamError::RateLimitExceeded.into()));
398                            } else {
399                                let deficit = n_elements - buffer.len();
400                                prefetch_demand += deficit;
401                                prefetch_completions.push_back((deficit, completion));
402                            }
403                        }
404                    }
405                }
406            }
407
408            result = items_rx.recv() => {
409                let Some(result) = result else {
410                    info!("{log_prefix} generator channel closed, shutting down");
411                    break;
412                };
413                match result {
414                    Ok(items) => {
415                        work_in_flight = false;
416                        let generated = items.len();
417                        debug!("{log_prefix} received {generated} items (pending_batches: {}, buffer: {})",
418                               pending_batches.iter().map(|(c, n, _)| n - c.len()).sum::<usize>(),
419                               buffer.len());
420                        let mut iter = items.into_iter();
421                        // 1. Fill outstanding batch requests in FIFO order.
422                        while let Some((ref mut collected, needed, _)) = pending_batches.front_mut() {
423                            let shortfall = *needed - collected.len();
424                            collected.extend(iter.by_ref().take(shortfall));
425                            if collected.len() < *needed { break; } // still waiting
426                            let (collected, _, tx) = pending_batches.pop_front().unwrap();
427                            let _ = tx.send(Ok(collected));
428                        }
429                        // 2. Buffer the rest; capacity is an admission bound, not a storage bound.
430                        buffer.extend(iter);
431                        // 3. Credit all newly generated items against prefetch deficits (FIFO).
432                        prefetch_demand = prefetch_demand.saturating_sub(generated);
433                        let mut credit = generated;
434                        while credit > 0 {
435                            let Some((deficit, _)) = prefetch_completions.front_mut() else { break; };
436                            let used = (*deficit).min(credit);
437                            *deficit -= used;
438                            credit -= used;
439                            if *deficit > 0 { break; }
440                            let (_, tx) = prefetch_completions.pop_front().unwrap();
441                            let _ = tx.send(Ok(()));
442                        }
443                        // A resync may have been waiting for this generation to free the work slot.
444                        maybe_skip(&mut pending_skip, &mut work_in_flight, &work_tx, &log_prefix);
445                    }
446                    Err(e) => {
447                        error!("{log_prefix} generation error, shutting down: {e:?}");
448                        for (_, _, tx) in pending_batches.drain(..) { let _ = tx.send(Err(e.clone())); }
449                        for (_, tx) in prefetch_completions.drain(..) { let _ = tx.send(Err(e.clone())); }
450                        if let Some((_, tx)) = pending_resync.take() { let _ = tx.send(Err(e.clone())); }
451                        return;
452                    }
453                }
454            }
455
456            result = skip_rx.recv() => {
457                let Some(result) = result else {
458                    info!("{log_prefix} skip channel closed, shutting down");
459                    break;
460                };
461                work_in_flight = false;
462                match result {
463                    Ok(()) => {
464                        // The generator advanced its position; finalize the in-flight resync.
465                        // Advancing by the deficit (rather than assigning the target) preserves
466                        // any next_n increments that landed while the skip was in flight.
467                        if let Some((deficit, tx)) = pending_resync.take() {
468                            shared_position.fetch_add(deficit as u64, Ordering::Release);
469                            let _ = tx.send(Ok(()));
470                        }
471                    }
472                    Err(e) => {
473                        error!("{log_prefix} skip error, shutting down: {e:?}");
474                        for (_, _, tx) in pending_batches.drain(..) { let _ = tx.send(Err(e.clone())); }
475                        for (_, tx) in prefetch_completions.drain(..) { let _ = tx.send(Err(e.clone())); }
476                        if let Some((_, tx)) = pending_resync.take() { let _ = tx.send(Err(e.clone())); }
477                        return;
478                    }
479                }
480            }
481        }
482
483        // After handling an event, (re)issue generation if there is unmet demand.
484        if let Err(e) = maybe_generate::<PB, E>(
485            &mut work_in_flight,
486            &pending_resync,
487            pending_skip,
488            &config,
489            &pending_batches,
490            buffer.len(),
491            prefetch_demand,
492            &work_tx,
493            &log_prefix,
494        ) {
495            error!("{log_prefix} config lock timeout, shutting down");
496            shutdown_err = Some(e);
497            break;
498        }
499    }
500
501    // Graceful shutdown: resolve all outstanding consumers/handles with the recorded error
502    // (if shutdown was triggered by a lock timeout) or `StreamClosed` otherwise.
503    let final_err: E = shutdown_err.unwrap_or_else(|| CorrelatedStreamError::StreamClosed.into());
504    for (_, _, tx) in pending_batches.drain(..) {
505        let _ = tx.send(Err(final_err.clone()));
506    }
507    for (_, tx) in prefetch_completions.drain(..) {
508        let _ = tx.send(Err(final_err.clone()));
509    }
510    if let Some((_, tx)) = pending_resync.take() {
511        let _ = tx.send(Err(final_err.clone()));
512    }
513}
514
515impl<
516        PB: CorrelatedBatch,
517        E: From<CorrelatedStreamError> + Clone + Send + std::fmt::Debug + 'static,
518    > CorrelatedStream<PB::Item> for BufferedStream<PB, E>
519{
520    type Error = E;
521
522    fn next_n(&self, n_elements: usize) -> Result<NextVec<PB::Item, E>, CorrelatedStreamError> {
523        if n_elements == 0 {
524            return Ok(NextVec::default());
525        }
526        let max_allowed = self.max_request_size()?;
527        if n_elements > max_allowed {
528            return Err(CorrelatedStreamError::RequestTooLarge {
529                requested: n_elements,
530                max_allowed,
531            });
532        }
533        let (tx, rx) = oneshot::channel();
534        self.command_sender
535            .send(Command::RequestN {
536                n_elements,
537                completion: tx,
538            })
539            .map_err(|e| CorrelatedStreamError::SendError(e.to_string()))?;
540        Ok(NextVec {
541            future: Next(rx),
542            size: n_elements,
543        })
544    }
545
546    fn prefetch_n(&self, n_elements: usize) -> PrefetchHandle<E> {
547        let (tx, rx) = oneshot::channel();
548        let max = match self.max_request_size() {
549            Ok(m) => m,
550            Err(e) => {
551                let _ = tx.send(Err(e.into()));
552                return PrefetchHandle::from(rx);
553            }
554        };
555        if n_elements > max {
556            let _ = tx.send(Err(CorrelatedStreamError::RequestTooLarge {
557                requested: n_elements,
558                max_allowed: max,
559            }
560            .into()));
561            return PrefetchHandle::from(rx);
562        }
563        // If the dispatcher is gone, resolve the handle immediately.
564        let cmd = Command::Prefetch {
565            n_elements,
566            completion: tx,
567        };
568        if let Err(e) = self.command_sender.send(cmd) {
569            if let Command::Prefetch { completion, .. } = e.0 {
570                let _ = completion.send(Err(CorrelatedStreamError::StreamClosed.into()));
571            }
572        }
573        PrefetchHandle::from(rx)
574    }
575
576    fn position(&self) -> u64 {
577        self.position.load(Ordering::Acquire)
578    }
579
580    fn resync(&self, target: u64) -> ResyncHandle<E> {
581        let (tx, rx) = oneshot::channel();
582        let cmd = Command::Resync {
583            target,
584            completion: tx,
585        };
586        if let Err(e) = self.command_sender.send(cmd) {
587            if let Command::Resync { completion, .. } = e.0 {
588                let _ = completion.send(Err(CorrelatedStreamError::StreamClosed.into()));
589            }
590        }
591        ResyncHandle::from(rx)
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use std::{
598        sync::{
599            atomic::{AtomicUsize, Ordering},
600            Arc,
601        },
602        time::Duration,
603    };
604
605    use rand::{rngs::StdRng, SeedableRng};
606    use typenum::U2;
607
608    use crate::{
609        algebra::elliptic_curve::{Curve25519Ristretto, ScalarField},
610        correlated_randomness::{
611            generator::CorrelationGenerator,
612            singlets::{Singlet, Singlets},
613            stream::{
614                buffered::{Buffer, BufferConfig, BufferedStream},
615                errors::CorrelatedStreamError,
616                CorrelatedStream,
617            },
618        },
619        random::Random,
620        utils::TryFuture,
621    };
622
623    type Fq = ScalarField<Curve25519Ristretto>;
624    type TestPB = Singlets<Fq, U2>;
625    type TestItem = Singlet<Fq>;
626    type TestErr = CorrelatedStreamError;
627
628    // -----------------------
629    // ===== Mock generator
630    // -----------------------
631
632    #[derive(Clone)]
633    struct MockGenConfig {
634        /// Sleep for this duration on every `run_for` call to simulate generation latency.
635        delay: Duration,
636        /// If `Some(threshold)`, fail (return `StreamClosed`) once this many items have been
637        /// generated cumulatively.
638        fail_after: Option<usize>,
639    }
640
641    impl Default for MockGenConfig {
642        fn default() -> Self {
643            Self {
644                delay: Duration::from_millis(0),
645                fail_after: None,
646            }
647        }
648    }
649
650    struct MockGen {
651        rng: StdRng,
652        cfg: MockGenConfig,
653        /// Total items produced (visible to tests via `Arc`).
654        items_produced: Arc<AtomicUsize>,
655        /// Number of `run_for` calls made (i.e. number of generation batches).
656        batches: Arc<AtomicUsize>,
657    }
658
659    impl MockGen {
660        fn new(cfg: MockGenConfig) -> (Self, Arc<AtomicUsize>, Arc<AtomicUsize>) {
661            let items_produced = Arc::new(AtomicUsize::new(0));
662            let batches = Arc::new(AtomicUsize::new(0));
663            let gen = Self {
664                rng: StdRng::from_seed([0u8; 32]),
665                cfg,
666                items_produced: items_produced.clone(),
667                batches: batches.clone(),
668            };
669            (gen, items_produced, batches)
670        }
671    }
672
673    impl CorrelationGenerator<TestPB> for MockGen {
674        type Net = ();
675        type Error = TestErr;
676
677        fn run(&mut self, _net: &mut ()) -> impl TryFuture<Ok = TestPB, Error = Self::Error> {
678            async move { Err(CorrelatedStreamError::StreamClosed) }
679        }
680
681        fn run_for(
682            &mut self,
683            n: usize,
684            _net: &mut (),
685        ) -> impl TryFuture<Ok = Vec<TestItem>, Error = Self::Error> {
686            async move {
687                self.batches.fetch_add(1, Ordering::SeqCst);
688                if self.cfg.delay > Duration::ZERO {
689                    tokio::time::sleep(self.cfg.delay).await;
690                }
691                if let Some(threshold) = self.cfg.fail_after {
692                    if self.items_produced.load(Ordering::SeqCst) + n > threshold {
693                        return Err(CorrelatedStreamError::StreamClosed);
694                    }
695                }
696                let items: Vec<TestItem> = (0..n)
697                    .map(|_| {
698                        Singlet::<Fq>::random_n::<Vec<_>>(&mut self.rng, 1)
699                            .into_iter()
700                            .next()
701                            .unwrap()
702                    })
703                    .collect();
704                self.items_produced.fetch_add(n, Ordering::SeqCst);
705                Ok(items)
706            }
707        }
708    }
709
710    fn make_stream(
711        cfg: MockGenConfig,
712        buf_cfg: BufferConfig,
713    ) -> (
714        BufferedStream<TestPB, TestErr>,
715        Arc<AtomicUsize>,
716        Arc<AtomicUsize>,
717    ) {
718        let (gen, produced, batches) = MockGen::new(cfg);
719        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), buf_cfg);
720        (stream, produced, batches)
721    }
722
723    // ----------------------------------------------------------------------
724    // ===== Skip-capable mock generator (dealer-like, deterministic-per-index)
725    // ----------------------------------------------------------------------
726
727    /// A generator that advertises [`CorrelationGenerator::SUPPORTS_UNILATERAL_SKIP`] and
728    /// implements `skip` by advancing its produced counter without emitting items.
729    struct SkipMockGen {
730        rng: StdRng,
731        produced: Arc<AtomicUsize>,
732        skipped: Arc<AtomicUsize>,
733    }
734
735    impl SkipMockGen {
736        fn new() -> (Self, Arc<AtomicUsize>, Arc<AtomicUsize>) {
737            let produced = Arc::new(AtomicUsize::new(0));
738            let skipped = Arc::new(AtomicUsize::new(0));
739            let gen = Self {
740                rng: StdRng::from_seed([7u8; 32]),
741                produced: produced.clone(),
742                skipped: skipped.clone(),
743            };
744            (gen, produced, skipped)
745        }
746    }
747
748    impl CorrelationGenerator<TestPB> for SkipMockGen {
749        type Net = ();
750        type Error = TestErr;
751
752        const SUPPORTS_UNILATERAL_SKIP: bool = true;
753
754        fn run(&mut self, _net: &mut ()) -> impl TryFuture<Ok = TestPB, Error = Self::Error> {
755            async move { Err(CorrelatedStreamError::StreamClosed) }
756        }
757
758        fn run_for(
759            &mut self,
760            n: usize,
761            _net: &mut (),
762        ) -> impl TryFuture<Ok = Vec<TestItem>, Error = Self::Error> {
763            async move {
764                let items: Vec<TestItem> = Singlet::<Fq>::random_n::<Vec<_>>(&mut self.rng, n);
765                self.produced.fetch_add(n, Ordering::SeqCst);
766                Ok(items)
767            }
768        }
769
770        fn skip(
771            &mut self,
772            n: usize,
773            _net: &mut (),
774        ) -> impl TryFuture<Ok = (), Error = Self::Error> {
775            async move {
776                // Advance the logical position by `n` without materializing the elements.
777                self.skipped.fetch_add(n, Ordering::SeqCst);
778                self.produced.fetch_add(n, Ordering::SeqCst);
779                Ok(())
780            }
781        }
782    }
783
784    /// A minimal generator with the default (false) skip capability, for testing the
785    /// `ResyncUnsupported` path when a deficit cannot be covered from the buffer.
786    struct NoSkipMockGen {
787        rng: StdRng,
788    }
789
790    impl CorrelationGenerator<TestPB> for NoSkipMockGen {
791        type Net = ();
792        type Error = TestErr;
793
794        fn run(&mut self, _net: &mut ()) -> impl TryFuture<Ok = TestPB, Error = Self::Error> {
795            async move { Err(CorrelatedStreamError::StreamClosed) }
796        }
797
798        fn run_for(
799            &mut self,
800            n: usize,
801            _net: &mut (),
802        ) -> impl TryFuture<Ok = Vec<TestItem>, Error = Self::Error> {
803            async move { Ok(Singlet::<Fq>::random_n::<Vec<_>>(&mut self.rng, n)) }
804        }
805    }
806
807    // -----------------------
808    // ===== Tests
809    // -----------------------
810
811    #[tokio::test]
812    async fn next_n_resolves_batch_future() {
813        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
814        let fut = stream.next_n(7).expect("request accepted");
815        let items = fut.await.expect("batch resolves");
816        assert_eq!(items.len(), 7);
817    }
818
819    #[tokio::test]
820    async fn request_too_large_rejected() {
821        let (stream, _, _) = make_stream(
822            MockGenConfig::default(),
823            BufferConfig::eager_with(8, 4), // capacity=8, max_request_size=4
824        );
825        match stream.next_n(5) {
826            Err(CorrelatedStreamError::RequestTooLarge {
827                requested: 5,
828                max_allowed: 4,
829            }) => {}
830            Ok(_) => panic!("must reject n>max"),
831            Err(e) => panic!("unexpected error: {e:?}"),
832        }
833    }
834
835    #[tokio::test]
836    async fn rate_limit_when_exceeding_capacity() {
837        // Small capacity + slow generator → the first batch's shortfall (4) plus the second
838        // request (4) exceeds capacity (4), tripping the admission bound.
839        let cfg = MockGenConfig {
840            delay: Duration::from_millis(200),
841            ..Default::default()
842        };
843        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(4));
844        let _f1 = stream.next_n(4).unwrap();
845        // Give the dispatcher a moment to register the first request before issuing the second.
846        tokio::time::sleep(Duration::from_millis(20)).await;
847        let f2 = stream.next_n(4).unwrap();
848        let results = futures::future::join_all(f2).await;
849        assert!(
850            results
851                .iter()
852                .all(|r| matches!(r, Err(CorrelatedStreamError::RateLimitExceeded))),
853            "expected all four to be rate-limited, got {results:?}"
854        );
855    }
856
857    #[tokio::test]
858    async fn prefetch_completes_and_serves_subsequent_requests_quickly() {
859        let cfg = MockGenConfig {
860            delay: Duration::from_millis(100),
861            ..Default::default()
862        };
863        let (stream, produced, _) = make_stream(cfg, BufferConfig::eager(32));
864        let handle = stream.prefetch_n(10);
865        handle.await.expect("prefetch completes");
866        assert!(produced.load(Ordering::SeqCst) >= 10);
867        // Subsequent request should be served from buffer instantaneously
868        // (or at least without waiting for another slow generation cycle).
869        let start = std::time::Instant::now();
870        let items = stream.next_n(10).unwrap().await.expect("served");
871        assert_eq!(items.len(), 10);
872        assert!(
873            start.elapsed() < Duration::from_millis(80),
874            "request should be served from prefetched buffer (took {:?})",
875            start.elapsed()
876        );
877    }
878
879    #[tokio::test]
880    async fn sequential_prefetches_all_resolve() {
881        // Regression: with refill_threshold=0 the first prefetch fills the buffer; the second
882        // must resolve immediately from the buffer instead of waiting for a generation target
883        // that never triggers.
884        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::lazy(16, 0));
885        for i in 0..2 {
886            tokio::time::timeout(Duration::from_secs(1), stream.prefetch_n(4))
887                .await
888                .unwrap_or_else(|_| panic!("prefetch {i} timed out"))
889                .expect("prefetch completes");
890        }
891        // A larger prefetch only partially covered by the buffer must also resolve.
892        tokio::time::timeout(Duration::from_secs(1), stream.prefetch_n(8))
893            .await
894            .expect("partially covered prefetch timed out")
895            .expect("prefetch completes");
896    }
897
898    #[tokio::test]
899    async fn generator_error_propagates_to_pending_consumers() {
900        let cfg = MockGenConfig {
901            delay: Duration::from_millis(20),
902            fail_after: Some(0), // fail on first batch
903        };
904        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(16));
905        let futs = stream.next_n(4).unwrap();
906        let results = futures::future::join_all(futs).await;
907        assert!(
908            results
909                .iter()
910                .all(|r| matches!(r, Err(CorrelatedStreamError::StreamClosed))),
911            "all consumers should receive the generator error"
912        );
913    }
914
915    #[tokio::test]
916    async fn fifo_order_across_two_batches() {
917        // Two requests issued back-to-back must be served in submission order.
918        let cfg = MockGenConfig {
919            delay: Duration::from_millis(40),
920            ..Default::default()
921        };
922        let (stream, _, batches) = make_stream(cfg, BufferConfig::eager(32));
923        let f1 = stream.next_n(3).unwrap();
924        let f2 = stream.next_n(3).unwrap();
925        let (a, b) = tokio::join!(f1, f2);
926        let a = a.expect("first batch resolves");
927        let b = b.expect("second batch resolves");
928        assert_eq!(a.len(), 3);
929        assert_eq!(b.len(), 3);
930        // At least one batch should have been generated (count is implementation-defined).
931        assert!(batches.load(Ordering::SeqCst) >= 1);
932    }
933
934    #[tokio::test]
935    async fn buffer_config_setters_are_visible() {
936        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
937        assert_eq!(stream.capacity().unwrap(), 16);
938        stream.set_capacity(32).unwrap();
939        assert_eq!(stream.capacity().unwrap(), 32);
940    }
941
942    // ── next_n validation
943    // ──────────────────────────────────────────────────────────────────────
944
945    #[tokio::test]
946    async fn next_n_rejects_too_large() {
947        let (stream, _, _) = make_stream(
948            MockGenConfig::default(),
949            BufferConfig::eager_with(8, 4), // capacity=8, max_request_size=4
950        );
951        match stream.next_n(5) {
952            Err(CorrelatedStreamError::RequestTooLarge {
953                requested: 5,
954                max_allowed: 4,
955            }) => {}
956            Ok(_) => panic!("must reject n > max"),
957            Err(e) => panic!("unexpected error: {e:?}"),
958        }
959    }
960
961    #[tokio::test]
962    async fn next_n_rate_limit_when_exceeding_capacity() {
963        // Slow generator → first batch's shortfall is outstanding; a second batch whose demand
964        // pushes the aggregate shortfall over capacity must be rejected with `RateLimitExceeded`.
965        let cfg = MockGenConfig {
966            delay: Duration::from_millis(200),
967            ..Default::default()
968        };
969        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(4));
970        let _f1 = stream.next_n(4).unwrap();
971        // Let the dispatcher register the first RequestBatch before issuing the second.
972        tokio::time::sleep(Duration::from_millis(20)).await;
973        let f2 = stream.next_n(4).unwrap();
974        assert!(
975            matches!(f2.await, Err(CorrelatedStreamError::RateLimitExceeded)),
976            "second next_n should be rate-limited"
977        );
978    }
979
980    #[tokio::test]
981    async fn next_n_admitted_when_shortfall_fits_capacity() {
982        // capacity=8, refill_threshold=0: a pending next_n(4) leaves a shortfall of 4, so a
983        // concurrent next_n(4) (aggregate 8) fits exactly and must be admitted, not rejected.
984        let cfg = MockGenConfig {
985            delay: Duration::from_millis(50),
986            ..Default::default()
987        };
988        let (stream, _, _) = make_stream(cfg, BufferConfig::lazy(8, 0));
989        let f1 = stream.next_n(4).unwrap();
990        tokio::time::sleep(Duration::from_millis(20)).await;
991        let f2 = stream.next_n(4).unwrap();
992        let (a, b) = tokio::join!(f1, f2);
993        assert_eq!(a.expect("first batch resolves").len(), 4);
994        assert_eq!(b.expect("second batch resolves").len(), 4);
995    }
996
997    #[tokio::test]
998    async fn next_n_error_propagates() {
999        // Generator always fails; the BatchFuture must resolve with the generator error.
1000        let cfg = MockGenConfig {
1001            delay: Duration::from_millis(20), // ensure batch is registered before failure
1002            fail_after: Some(0),
1003        };
1004        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(16));
1005        let result = stream.next_n(4).unwrap().await;
1006        assert!(
1007            matches!(result, Err(CorrelatedStreamError::StreamClosed)),
1008            "expected generator error to propagate through BatchFuture, got {result:?}"
1009        );
1010    }
1011
1012    // ── refill_threshold ───────────────────────────────────────────────────────────────────────
1013
1014    #[tokio::test]
1015    async fn refill_threshold_drives_proactive_generation() {
1016        // lazy buffer: capacity=16, refill_threshold=8 → after a 4-item request, the dispatcher
1017        // should top up to 8 items in the buffer.
1018        let (stream, produced, _) =
1019            make_stream(MockGenConfig::default(), BufferConfig::lazy(16, 8));
1020        let _ = stream.next_n(4).unwrap().await.expect("served");
1021        // Allow the dispatcher to settle (post-serve refill cycle).
1022        tokio::time::sleep(Duration::from_millis(50)).await;
1023        // We requested 4 items; refill_threshold=8 means the buffer should hold at least 8
1024        // additional items beyond what was served, i.e. >= 12 total generated.
1025        let total = produced.load(Ordering::SeqCst);
1026        assert!(total >= 8, "expected >= 8 items generated, got {total}");
1027    }
1028
1029    // ── position counter & resync ────────────────────────────────────────────────────────────
1030
1031    #[tokio::test]
1032    async fn position_tracks_delivered_not_prefetched() {
1033        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(32));
1034        // Prefetching fills the buffer but must NOT advance the logical position.
1035        stream.prefetch_n(10).await.expect("prefetch completes");
1036        assert_eq!(stream.position(), 0, "prefetch must not advance position");
1037        // Delivering through next_n advances it by exactly the requested amount.
1038        let _ = stream.next_n(7).unwrap().await.expect("served");
1039        assert_eq!(stream.position(), 7);
1040        let _ = stream.next_n(3).unwrap().await.expect("served");
1041        assert_eq!(stream.position(), 10);
1042    }
1043
1044    #[tokio::test]
1045    async fn position_does_not_advance_on_rejected_request() {
1046        // Slow generator + tiny capacity → the second request is rate-limited and delivers
1047        // nothing, so the position must stay at the first request's amount.
1048        let cfg = MockGenConfig {
1049            delay: Duration::from_millis(200),
1050            ..Default::default()
1051        };
1052        let (stream, _, _) = make_stream(cfg, BufferConfig::eager(4));
1053        let _f1 = stream.next_n(4).unwrap();
1054        tokio::time::sleep(Duration::from_millis(20)).await;
1055        let f2 = stream.next_n(4).unwrap();
1056        assert!(matches!(
1057            f2.await,
1058            Err(CorrelatedStreamError::RateLimitExceeded)
1059        ));
1060        // Only the admitted first request counts.
1061        assert_eq!(stream.position(), 4);
1062    }
1063
1064    #[tokio::test]
1065    async fn resync_drains_buffer_and_advances_position() {
1066        // Lazy buffer with no proactive refill, so the only generation is the explicit prefetch;
1067        // this isolates resync's drain from background top-ups.
1068        let (stream, produced, _) =
1069            make_stream(MockGenConfig::default(), BufferConfig::lazy(32, 0));
1070        stream.prefetch_n(10).await.expect("prefetch completes");
1071        let before = produced.load(Ordering::SeqCst);
1072        // Target is fully covered by the buffer: pure local drain, no extra generation.
1073        stream.resync(6).await.expect("resync completes");
1074        assert_eq!(stream.position(), 6);
1075        assert_eq!(
1076            produced.load(Ordering::SeqCst),
1077            before,
1078            "drain-only resync must not generate"
1079        );
1080        // Subsequent delivery continues from the resynced position.
1081        let _ = stream.next_n(2).unwrap().await.expect("served");
1082        assert_eq!(stream.position(), 8);
1083    }
1084
1085    #[tokio::test]
1086    async fn resync_noop_when_already_at_target() {
1087        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
1088        let _ = stream.next_n(5).unwrap().await.expect("served");
1089        stream.resync(5).await.expect("no-op resync completes");
1090        assert_eq!(stream.position(), 5);
1091    }
1092
1093    #[tokio::test]
1094    async fn resync_rewind_is_rejected() {
1095        let (stream, _, _) = make_stream(MockGenConfig::default(), BufferConfig::eager(16));
1096        let _ = stream.next_n(5).unwrap().await.expect("served");
1097        match stream.resync(3).await {
1098            Err(CorrelatedStreamError::ResyncRewind {
1099                current: 5,
1100                target: 3,
1101            }) => {}
1102            other => panic!("expected ResyncRewind, got {other:?}"),
1103        }
1104        // Position is unchanged after a rejected rewind.
1105        assert_eq!(stream.position(), 5);
1106    }
1107
1108    #[tokio::test]
1109    async fn resync_unsupported_when_deficit_and_no_skip() {
1110        // Lazy buffer with no proactive refill so nothing is buffered; a resync past the buffer
1111        // needs generator skipping, which this generator does not support.
1112        let gen = NoSkipMockGen {
1113            rng: StdRng::from_seed([0u8; 32]),
1114        };
1115        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), BufferConfig::lazy(16, 0));
1116        match stream.resync(10).await {
1117            Err(CorrelatedStreamError::ResyncUnsupported {
1118                generated: 0,
1119                target: 10,
1120            }) => {}
1121            other => panic!("expected ResyncUnsupported, got {other:?}"),
1122        }
1123        // Nothing was delivered/skipped, so the position stays put.
1124        assert_eq!(stream.position(), 0);
1125    }
1126
1127    #[tokio::test]
1128    async fn resync_skips_at_generator_when_supported() {
1129        let (gen, produced, skipped) = SkipMockGen::new();
1130        // Lazy buffer, no proactive refill: the resync deficit must be skipped at the generator.
1131        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), BufferConfig::lazy(64, 0));
1132        stream.resync(10).await.expect("resync via skip completes");
1133        assert_eq!(stream.position(), 10);
1134        assert_eq!(
1135            skipped.load(Ordering::SeqCst),
1136            10,
1137            "deficit should be skipped"
1138        );
1139        assert_eq!(
1140            produced.load(Ordering::SeqCst),
1141            10,
1142            "skip advances the generator without delivering items"
1143        );
1144        // Delivery resumes correctly from the skipped-to position.
1145        let items = stream.next_n(3).unwrap().await.expect("served");
1146        assert_eq!(items.len(), 3);
1147        assert_eq!(stream.position(), 13);
1148    }
1149
1150    #[tokio::test]
1151    async fn resync_partial_buffer_then_skip_remainder() {
1152        let (gen, produced, skipped) = SkipMockGen::new();
1153        let stream = BufferedStream::<TestPB, TestErr>::new(gen, (), BufferConfig::lazy(64, 0));
1154        // Buffer 4 elements, then resync past them: 4 drained locally, 6 skipped at generator.
1155        stream.prefetch_n(4).await.expect("prefetch completes");
1156        let produced_after_prefetch = produced.load(Ordering::SeqCst);
1157        assert_eq!(produced_after_prefetch, 4);
1158        stream.resync(10).await.expect("resync completes");
1159        assert_eq!(stream.position(), 10);
1160        assert_eq!(
1161            skipped.load(Ordering::SeqCst),
1162            6,
1163            "only the unbuffered remainder is skipped at the generator"
1164        );
1165    }
1166}