Skip to main content

vortex_file/segments/
source.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::any::Any;
5use std::collections::VecDeque;
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::sync::atomic::AtomicUsize;
10use std::sync::atomic::Ordering;
11use std::task::Context;
12use std::task::Poll;
13
14use futures::FutureExt;
15use futures::Stream;
16use futures::StreamExt;
17use futures::channel::mpsc;
18use futures::channel::oneshot;
19use futures::future;
20use futures::future::BoxFuture;
21use futures::future::Shared;
22use futures::stream::BoxStream;
23use futures::stream::Fuse;
24use futures::stream::SelectAll;
25use parking_lot::Mutex;
26use vortex_array::buffer::BufferHandle;
27use vortex_buffer::Alignment;
28use vortex_buffer::ByteBuffer;
29use vortex_error::VortexExpect;
30use vortex_error::VortexResult;
31use vortex_error::vortex_err;
32use vortex_error::vortex_panic;
33use vortex_io::ReadAtRequest;
34use vortex_io::ReadAtStream;
35use vortex_io::VortexReadAt;
36use vortex_io::runtime::Handle;
37use vortex_io::runtime::JoinOutcome;
38use vortex_layout::segments::SegmentFuture;
39use vortex_layout::segments::SegmentId;
40use vortex_layout::segments::SegmentSource;
41use vortex_metrics::Counter;
42use vortex_metrics::Histogram;
43use vortex_metrics::Label;
44use vortex_metrics::MetricBuilder;
45use vortex_metrics::MetricsRegistry;
46
47use crate::SegmentSpec;
48use crate::read::IoRequest;
49use crate::read::IoRequestStream;
50use crate::read::ReadRequest;
51use crate::read::RequestId;
52
53#[derive(Debug)]
54/// Events sent from segment futures to the coalescing read driver.
55pub enum ReadEvent {
56    /// A segment read has been registered.
57    Request(ReadRequest),
58    /// A registered read future has been polled.
59    Polled(RequestId),
60    /// A registered read future was dropped before completion.
61    Dropped(RequestId),
62}
63
64/// A [`SegmentSource`] for file-like IO.
65/// ## Coalescing and Pre-fetching
66///
67/// It is important to understand the semantics of the read futures returned by a [`FileSegmentSource`].
68/// Under the hood, each instance is backed by a stream that services read requests by
69/// applying coalescing and concurrency constraints.
70///
71/// Each read future has four states:
72/// * `registered` - the read future has been created, but not yet polled.
73/// * `requested` - the read future has been polled.
74/// * `in-flight` - the read request has been sent to the underlying storage system.
75/// * `resolved` - the read future has completed and resolved a result.
76///
77/// When a read request is `registered`, it will not itself trigger any I/O, but is eligible to
78/// be coalesced with other requests.
79///
80/// If a read future is dropped, it will be canceled if possible. This depends on the current
81/// state of the request, as well as whether the underlying storage system supports cancellation.
82///
83/// I/O requests will be processed in the order they are `registered`, however coalescing may mean
84/// other registered requests are lumped together into a single I/O operation.
85/// A cloneable handle to the background read driver, shared by every in-flight [`ReadFuture`].
86///
87/// [`Shared`] fans a single completion out to all readers with correct waker bookkeeping, so a
88/// reader is always woken when the driver finishes — even if another reader polled the driver more
89/// recently and was then dropped. Its output is `()`; a driver panic is carried out of band in
90/// [`DriverPanic`] so it can be re-raised on the reader side.
91type SharedDriver = Shared<BoxFuture<'static, ()>>;
92
93/// Slot holding the driver's panic payload, if it panicked while driving reads. The first reader to
94/// observe completion takes the payload and re-raises it; later readers report a graceful error.
95type DriverPanic = Arc<Mutex<Option<Box<dyn Any + Send>>>>;
96
97fn validate_read_result(
98    request: &IoRequest,
99    result: VortexResult<BufferHandle>,
100) -> VortexResult<BufferHandle> {
101    result.and_then(|buffer| {
102        if request.len() != buffer.len() {
103            return Err(vortex_err!(
104                "FileSegmentSource: expected buffer of length {} but received {}. {:?}",
105                request.len(),
106                buffer.len(),
107                request
108            ));
109        }
110        Ok(buffer)
111    })
112}
113
114type IoBatchStream = Fuse<BoxStream<'static, Vec<IoRequest>>>;
115
116enum ReadRangeResultsState {
117    Reading(ReadAtStream),
118    Missing,
119}
120
121/// Matches streamed range results back to their logical requests.
122struct ReadRangeResults {
123    state: ReadRangeResultsState,
124    remaining: Vec<Option<IoRequest>>,
125}
126
127impl ReadRangeResults {
128    fn new(results: ReadAtStream, requests: Vec<IoRequest>) -> Self {
129        Self {
130            state: ReadRangeResultsState::Reading(results),
131            remaining: requests.into_iter().map(Some).collect(),
132        }
133    }
134}
135
136impl Stream for ReadRangeResults {
137    type Item = (IoRequest, VortexResult<BufferHandle>);
138
139    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
140        loop {
141            match &mut self.state {
142                ReadRangeResultsState::Reading(results) => match results.poll_next_unpin(cx) {
143                    Poll::Ready(Some((request, result))) => {
144                        let Some(position) = self.remaining.iter().position(|req| {
145                            req.as_ref().is_some_and(|req| {
146                                req.offset() == request.offset
147                                    && req.len() == request.length
148                                    && req.alignment() == request.alignment
149                            })
150                        }) else {
151                            tracing::warn!(?request, "reader returned an unknown range");
152                            continue;
153                        };
154                        let req = self.remaining[position]
155                            .take()
156                            .vortex_expect("matched request is present");
157                        return Poll::Ready(Some((req, result)));
158                    }
159                    Poll::Ready(None) => self.state = ReadRangeResultsState::Missing,
160                    Poll::Pending => return Poll::Pending,
161                },
162                ReadRangeResultsState::Missing => {
163                    let Some(req) = self.remaining.iter_mut().find_map(Option::take) else {
164                        return Poll::Ready(None);
165                    };
166                    let error = vortex_err!(
167                        "FileSegmentSource: read_ranges ended before resolving request. {:?}",
168                        req
169                    );
170                    return Poll::Ready(Some((req, Err(error))));
171                }
172            }
173        }
174    }
175}
176
177/// Drives request batches while keeping the reader's concurrency slots occupied.
178struct ReadDriver<R> {
179    reader: Arc<R>,
180    batches: IoBatchStream,
181    pending: VecDeque<IoRequest>,
182    reads: SelectAll<ReadRangeResults>,
183    num_active: usize,
184    batches_done: bool,
185    concurrency: usize,
186    metrics: RequestMetrics,
187}
188
189impl<R: VortexReadAt> ReadDriver<R> {
190    fn new(
191        reader: R,
192        batches: BoxStream<'static, Vec<IoRequest>>,
193        concurrency: usize,
194        metrics: RequestMetrics,
195    ) -> Self {
196        Self {
197            reader: Arc::new(reader),
198            batches: batches.fuse(),
199            pending: VecDeque::new(),
200            reads: SelectAll::new(),
201            num_active: 0,
202            batches_done: false,
203            concurrency,
204            metrics,
205        }
206    }
207
208    fn submit_pending(&mut self) {
209        while self.num_active < self.concurrency && !self.pending.is_empty() {
210            let batch_len = (self.concurrency - self.num_active).min(self.pending.len());
211            let reqs = self.pending.drain(..batch_len).collect::<Vec<_>>();
212            self.num_active += batch_len;
213
214            self.metrics.read_ranges_calls.add(1);
215            self.metrics.read_ranges_num_ranges.update(batch_len as f64);
216            if batch_len > 1 {
217                self.metrics.read_ranges_multi.add(1);
218            }
219            tracing::trace!(
220                target: "vortex_file::read_ranges",
221                num_ranges = batch_len,
222                num_active = self.num_active,
223                "submitting positional read batch"
224            );
225
226            let requests = reqs
227                .iter()
228                .map(|req| ReadAtRequest::new(req.offset(), req.len(), req.alignment()))
229                .collect::<Vec<_>>()
230                .into();
231            let results = self.reader.read_ranges(requests);
232            self.reads.push(ReadRangeResults::new(results, reqs));
233        }
234    }
235}
236
237impl<R: VortexReadAt> Stream for ReadDriver<R> {
238    type Item = ();
239
240    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
241        let this = self.as_mut().get_mut();
242
243        // Observe every batch already available so submission can fill all free slots at once.
244        if !this.batches_done {
245            loop {
246                match this.batches.poll_next_unpin(cx) {
247                    Poll::Ready(Some(batch)) => this.pending.extend(batch),
248                    Poll::Ready(None) => {
249                        this.batches_done = true;
250                        break;
251                    }
252                    Poll::Pending => break,
253                }
254            }
255        }
256
257        this.submit_pending();
258
259        if this.batches_done && this.num_active == 0 {
260            return Poll::Ready(None);
261        }
262
263        match this.reads.poll_next_unpin(cx) {
264            Poll::Ready(Some((req, result))) => {
265                this.num_active -= 1;
266                let result = validate_read_result(&req, result);
267                req.resolve(result);
268                Poll::Ready(Some(()))
269            }
270            Poll::Ready(None) if this.num_active == 0 => Poll::Pending,
271            Poll::Ready(None) => {
272                vortex_panic!("read result streams ended with active requests")
273            }
274            Poll::Pending => Poll::Pending,
275        }
276    }
277}
278
279pub struct FileSegmentSource {
280    segments: Arc<[SegmentSpec]>,
281    /// A queue for sending read request events to the I/O stream.
282    events: mpsc::UnboundedSender<ReadEvent>,
283    /// Background request driver, joined by readers to surface a driver panic.
284    driver: SharedDriver,
285    /// Panic payload captured if the driver panicked while driving reads.
286    driver_panic: DriverPanic,
287    /// The next read request ID.
288    next_id: Arc<AtomicUsize>,
289}
290
291impl FileSegmentSource {
292    /// Open a file-backed segment source over `reader`.
293    ///
294    /// The returned source spawns a background driver on `handle` that coalesces and executes
295    /// random-access read requests.
296    pub fn open<R: VortexReadAt + Clone>(
297        segments: Arc<[SegmentSpec]>,
298        reader: R,
299        handle: Handle,
300        metrics: RequestMetrics,
301    ) -> Self {
302        let (send, recv) = mpsc::unbounded();
303
304        let max_alignment = segments
305            .iter()
306            .map(|segment| segment.alignment)
307            .max()
308            .unwrap_or_else(Alignment::none);
309        let coalesce_config = reader.coalesce_config().map(|mut config| {
310            // Aligning the coalesced start down can add up to (alignment - 1) bytes.
311            // Increase max_size to keep the effective payload window consistent.
312            let extra = (max_alignment.as_usize() as u64).saturating_sub(1);
313            config.max_size = config.max_size.saturating_add(extra);
314            config
315        });
316        let concurrency = reader.concurrency();
317        if concurrency == 0 {
318            vortex_panic!(
319                "VortexReadAt::concurrency returned 0 (uri={:?}); this would stall I/O",
320                reader.uri()
321            );
322        }
323
324        let stream = IoRequestStream::new(
325            StreamExt::boxed(recv),
326            coalesce_config,
327            max_alignment,
328            concurrency,
329            metrics.clone(),
330        )
331        .boxed();
332
333        let drive_fut = ReadDriver::new(reader, stream, concurrency, metrics).collect::<()>();
334
335        // Spawn the driver so the runtime makes I/O progress independently of any reader. Readers
336        // join it (below) only to surface a panic raised while driving reads.
337        let mut task = handle.spawn(drive_fut);
338        let driver_panic: DriverPanic = Arc::new(Mutex::new(None));
339        let driver = {
340            let driver_panic = Arc::clone(&driver_panic);
341            async move {
342                // Poll for the terminal outcome without re-raising: a benign abort (runtime
343                // teardown) resolves to `()` so readers report a graceful error, while a panic is
344                // stashed for the first reader to re-raise.
345                if let JoinOutcome::Panicked(panic) = future::poll_fn(|cx| task.poll_join(cx)).await
346                {
347                    *driver_panic.lock() = Some(panic);
348                }
349            }
350            .boxed()
351            .shared()
352        };
353
354        Self {
355            segments,
356            events: send,
357            driver,
358            driver_panic,
359            next_id: Arc::new(AtomicUsize::new(0)),
360        }
361    }
362}
363
364impl SegmentSource for FileSegmentSource {
365    fn request(&self, id: SegmentId) -> SegmentFuture {
366        // We eagerly register the read request here assuming the behaviour of [`FileSegmentSource`], where
367        // coalescing becomes effective prior to the future being polled.
368        let spec = *match self.segments.get(*id as usize) {
369            Some(spec) => spec,
370            None => {
371                return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
372            }
373        };
374
375        let SegmentSpec {
376            offset,
377            length,
378            alignment,
379        } = spec;
380
381        let (send, recv) = oneshot::channel();
382        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
383        let event = ReadEvent::Request(ReadRequest {
384            id,
385            offset,
386            length: length as usize,
387            alignment,
388            callback: send,
389        });
390
391        // If we fail to submit the event, we create a future that has failed.
392        if let Err(e) = self.events.unbounded_send(event) {
393            return future::ready(Err(vortex_err!("Failed to submit read request: {e}"))).boxed();
394        }
395
396        let fut = ReadFuture {
397            id,
398            recv,
399            polled: false,
400            finished: false,
401            events: self.events.clone(),
402            driver: self.driver.clone(),
403            driver_panic: Arc::clone(&self.driver_panic),
404        };
405
406        // One allocation: we only box the returned SegmentFuture, not the inner ReadFuture.
407        fut.boxed()
408    }
409}
410
411/// A future that resolves a read request from a [`FileSegmentSource`].
412///
413/// See the documentation for [`FileSegmentSource`] for details on coalescing and pre-fetching.
414/// If dropped, the read request will be canceled where possible.
415struct ReadFuture {
416    id: usize,
417    recv: oneshot::Receiver<VortexResult<BufferHandle>>,
418    polled: bool,
419    finished: bool,
420    events: mpsc::UnboundedSender<ReadEvent>,
421    driver: SharedDriver,
422    driver_panic: DriverPanic,
423}
424
425impl Future for ReadFuture {
426    type Output = VortexResult<BufferHandle>;
427
428    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
429        match self.recv.poll_unpin(cx) {
430            // note: we are skipping polled and dropped events for this if the future is ready on
431            //       the first poll, that means this request was completed before it was polled,
432            //       as part of a coalesced request.
433            Poll::Ready(Ok(result)) => {
434                self.finished = true;
435                Poll::Ready(result)
436            }
437            // The request's sender was dropped, so the driver has finished. Join it so a panic
438            // raised while driving reads is re-raised here rather than surfacing as a generic
439            // error. Only report the dropped error once the driver has finished.
440            Poll::Ready(Err(e)) => match self.driver.poll_unpin(cx) {
441                Poll::Ready(()) => {
442                    self.finished = true;
443                    // Re-raise the driver panic on the first reader to observe it; later readers
444                    // fall through to the graceful dropped error.
445                    if let Some(panic) = self.driver_panic.lock().take() {
446                        std::panic::resume_unwind(panic);
447                    }
448                    Poll::Ready(Err(vortex_err!("ReadRequest dropped by runtime: {e}")))
449                }
450                Poll::Pending => Poll::Pending,
451            },
452            Poll::Pending if !self.polled => {
453                self.polled = true;
454                // Notify the I/O stream that this request has been polled.
455                match self.events.unbounded_send(ReadEvent::Polled(self.id)) {
456                    Ok(()) => Poll::Pending,
457                    Err(e) => Poll::Ready(Err(vortex_err!("ReadRequest dropped by runtime: {e}"))),
458                }
459            }
460            _ => Poll::Pending,
461        }
462    }
463}
464
465impl Drop for ReadFuture {
466    fn drop(&mut self) {
467        // Completed requests have already left driver state.
468        if self.finished {
469            return;
470        }
471
472        // Best-effort cancellation signal to the I/O stream.
473        drop(self.events.unbounded_send(ReadEvent::Dropped(self.id)));
474    }
475}
476
477/// Metrics emitted by the file segment request driver.
478#[derive(Clone)]
479pub struct RequestMetrics {
480    /// Number of individual segment requests observed by the driver.
481    pub individual_requests: Counter,
482    /// Number of physical reads after coalescing.
483    pub coalesced_requests: Counter,
484    /// Distribution of how many segment requests were merged into each physical read.
485    pub num_requests_coalesced: Histogram,
486    /// Number of calls made to [`VortexReadAt::read_ranges`].
487    pub read_ranges_calls: Counter,
488    /// Number of `read_ranges` calls containing more than one physical range.
489    pub read_ranges_multi: Counter,
490    /// Distribution of physical range counts submitted per `read_ranges` call.
491    pub read_ranges_num_ranges: Histogram,
492}
493
494impl RequestMetrics {
495    /// Create request metrics in `metrics_registry` with shared labels.
496    pub fn new(metrics_registry: &dyn MetricsRegistry, labels: Vec<Label>) -> Self {
497        Self {
498            individual_requests: MetricBuilder::new(metrics_registry)
499                .add_labels(labels.clone())
500                .counter("io.requests.individual"),
501            coalesced_requests: MetricBuilder::new(metrics_registry)
502                .add_labels(labels.clone())
503                .counter("io.requests.coalesced"),
504            num_requests_coalesced: MetricBuilder::new(metrics_registry)
505                .add_labels(labels.clone())
506                .histogram("io.requests.coalesced.num_coalesced"),
507            read_ranges_calls: MetricBuilder::new(metrics_registry)
508                .add_labels(labels.clone())
509                .counter("io.read_ranges.calls"),
510            read_ranges_multi: MetricBuilder::new(metrics_registry)
511                .add_labels(labels.clone())
512                .counter("io.read_ranges.multi_range_calls"),
513            read_ranges_num_ranges: MetricBuilder::new(metrics_registry)
514                .add_labels(labels)
515                .histogram("io.read_ranges.num_ranges"),
516        }
517    }
518}
519
520/// A [`SegmentSource`] that resolves segments synchronously from an
521/// in-memory [`ByteBuffer`].
522///
523/// Resolves segments synchronously, bypassing the async I/O pipeline.
524pub(crate) struct BufferSegmentSource {
525    buffer: ByteBuffer,
526    segments: Arc<[SegmentSpec]>,
527}
528
529impl BufferSegmentSource {
530    /// Create a new `BufferSegmentSource` from a buffer and its segment map.
531    pub fn new(buffer: ByteBuffer, segments: Arc<[SegmentSpec]>) -> Self {
532        Self { buffer, segments }
533    }
534}
535
536impl SegmentSource for BufferSegmentSource {
537    fn request(&self, id: SegmentId) -> SegmentFuture {
538        let spec = match self.segments.get(*id as usize) {
539            Some(spec) => spec,
540            None => {
541                return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
542            }
543        };
544
545        let start = spec.offset as usize;
546        let end = start + spec.length as usize;
547        if end > self.buffer.len() {
548            return future::ready(Err(vortex_err!(
549                "Segment {} range {}..{} out of bounds for buffer of length {}",
550                *id,
551                start,
552                end,
553                self.buffer.len()
554            )))
555            .boxed();
556        }
557
558        let slice = self.buffer.slice(start..end).aligned(spec.alignment);
559        future::ready(Ok(BufferHandle::new_host(slice))).boxed()
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use std::panic::AssertUnwindSafe;
566    use std::sync::atomic::AtomicBool;
567
568    use futures::future::BoxFuture;
569    use futures::task::ArcWake;
570    use vortex_error::vortex_bail;
571    use vortex_io::runtime::tokio::TokioRuntime;
572    use vortex_layout::segments::SegmentSource;
573    use vortex_metrics::DefaultMetricsRegistry;
574
575    use super::*;
576
577    fn io_request(id: RequestId, offset: u64, length: usize) -> IoRequest {
578        let (callback, _receiver) = oneshot::channel();
579        IoRequest::new_single(ReadRequest {
580            id,
581            offset,
582            length,
583            alignment: Alignment::none(),
584            callback,
585        })
586    }
587
588    #[tokio::test]
589    async fn read_range_results_matches_results_and_reports_missing_requests() {
590        let requests = vec![io_request(0, 0, 4), io_request(1, 4, 4)];
591        let unknown = ReadAtRequest::new(8, 4, Alignment::none());
592        let returned = ReadAtRequest::new(4, 4, Alignment::none());
593        let buffer = BufferHandle::new_host(ByteBuffer::from(vec![0; 4]));
594        let results =
595            futures::stream::iter([(unknown, Ok(buffer.clone())), (returned, Ok(buffer))]).boxed();
596
597        let resolved = ReadRangeResults::new(results, requests)
598            .collect::<Vec<_>>()
599            .await;
600
601        assert_eq!(resolved.len(), 2);
602        assert_eq!(resolved[0].0.offset(), 4);
603        assert!(resolved[0].1.is_ok());
604        assert_eq!(resolved[1].0.offset(), 0);
605        assert!(resolved[1].1.is_err());
606    }
607
608    #[derive(Clone)]
609    struct PanickingReadAt;
610
611    impl VortexReadAt for PanickingReadAt {
612        fn concurrency(&self) -> usize {
613            1
614        }
615
616        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
617            async { Ok(4) }.boxed()
618        }
619
620        fn read_at(
621            &self,
622            _offset: u64,
623            _length: usize,
624            _alignment: Alignment,
625        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
626            async {
627                panic!("read-at panic");
628            }
629            .boxed()
630        }
631    }
632
633    fn panicking_source() -> FileSegmentSource {
634        let segments: Arc<[SegmentSpec]> = Arc::from([SegmentSpec {
635            offset: 0,
636            length: 4,
637            alignment: Alignment::none(),
638        }]);
639        let metrics = DefaultMetricsRegistry::default();
640        FileSegmentSource::open(
641            segments,
642            PanickingReadAt,
643            TokioRuntime::current(),
644            RequestMetrics::new(&metrics, vec![]),
645        )
646    }
647
648    fn panic_message(payload: &(dyn Any + Send)) -> &str {
649        payload
650            .downcast_ref::<&str>()
651            .copied()
652            .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
653            .unwrap_or("<non-string panic>")
654    }
655
656    #[tokio::test]
657    #[should_panic(expected = "read-at panic")]
658    async fn file_segment_source_propagates_read_driver_panic() {
659        let source = panicking_source();
660        let _result = source.request(SegmentId::from(0)).await;
661    }
662
663    // A read-driver panic must propagate on *every* run rather than sometimes surfacing as a
664    // generic "dropped by runtime" error, which is nondeterministic.
665    #[tokio::test]
666    async fn file_segment_source_read_driver_panic_propagates_deterministically() {
667        for i in 0..100 {
668            let source = panicking_source();
669            let outcome = AssertUnwindSafe(source.request(SegmentId::from(0)))
670                .catch_unwind()
671                .await;
672            assert!(
673                outcome.is_err(),
674                "read-driver panic was not propagated on iteration {i}; \
675                 the request resolved instead of panicking"
676            );
677        }
678    }
679
680    #[derive(Default)]
681    struct WakeFlag(AtomicBool);
682
683    impl ArcWake for WakeFlag {
684        fn wake_by_ref(arc_self: &Arc<Self>) {
685            arc_self.0.store(true, Ordering::SeqCst);
686        }
687    }
688
689    // A driver panic must surface to every reader sharing one driver: exactly one reader re-raises
690    // the original panic, the rest report a graceful error, and none hang. The readers are polled
691    // by hand, so a missed wake-up fails an assertion instead of a wall-clock timeout. The panic
692    // hook can take seconds to symbolize a backtrace on Windows.
693    #[tokio::test]
694    async fn file_segment_source_panic_propagates_to_all_concurrent_readers() {
695        let source = panicking_source();
696
697        let mut readers: Vec<_> = (0..8)
698            .map(|_| {
699                (
700                    source.request(SegmentId::from(0)),
701                    Arc::new(WakeFlag::default()),
702                )
703            })
704            .collect();
705
706        let poll_reader = |reader: &mut SegmentFuture, flag: &Arc<WakeFlag>| {
707            let waker = futures::task::waker(Arc::clone(flag));
708            let mut cx = Context::from_waker(&waker);
709            std::panic::catch_unwind(AssertUnwindSafe(|| reader.poll_unpin(&mut cx)))
710        };
711
712        // The first poll marks each request as polled, which lets the driver issue the read.
713        for (reader, flag) in &mut readers {
714            assert!(
715                matches!(poll_reader(reader, flag), Ok(Poll::Pending)),
716                "a reader resolved before the driver ran"
717            );
718        }
719
720        source.driver.clone().await;
721
722        let mut original_panics = 0;
723        for (i, (reader, flag)) in readers.iter_mut().enumerate() {
724            assert!(
725                flag.0.load(Ordering::SeqCst),
726                "reader {i} was not woken after the driver finished"
727            );
728
729            let outcome = match poll_reader(reader, flag) {
730                Ok(Poll::Ready(result)) => Ok(result),
731                Ok(Poll::Pending) => {
732                    panic!("reader {i} is still pending after the driver finished")
733                }
734                Err(payload) => Err(payload),
735            };
736
737            match outcome {
738                // The first reader to observe completion re-raises the original panic.
739                Err(payload) => {
740                    assert!(
741                        panic_message(&*payload).contains("read-at panic"),
742                        "got: {:?}",
743                        panic_message(&*payload)
744                    );
745                    original_panics += 1;
746                }
747                // Every other reader reports a graceful dropped-by-runtime error.
748                Ok(result) => assert!(result.is_err(), "expected a dropped-by-runtime error"),
749            }
750        }
751
752        assert_eq!(
753            original_panics, 1,
754            "exactly one reader should re-raise the original driver panic"
755        );
756    }
757
758    #[derive(Clone)]
759    struct ReadRangesOnly {
760        calls: Arc<AtomicUsize>,
761    }
762
763    impl VortexReadAt for ReadRangesOnly {
764        fn concurrency(&self) -> usize {
765            4
766        }
767
768        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
769            async { Ok(16) }.boxed()
770        }
771
772        fn read_at(
773            &self,
774            _offset: u64,
775            _length: usize,
776            _alignment: Alignment,
777        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
778            async { panic!("read_at should not be called") }.boxed()
779        }
780
781        fn read_ranges(&self, requests: Arc<[ReadAtRequest]>) -> ReadAtStream {
782            self.calls.fetch_add(1, Ordering::Relaxed);
783            let results = requests
784                .iter()
785                .copied()
786                .map(|request| {
787                    let buffer = BufferHandle::new_host(
788                        ByteBuffer::from(vec![0; request.length]).aligned(request.alignment),
789                    );
790                    (request, Ok(buffer))
791                })
792                .collect::<Vec<_>>();
793            futures::stream::iter(results).boxed()
794        }
795    }
796
797    #[tokio::test]
798    async fn read_driver_batches_ready_requests() -> VortexResult<()> {
799        let calls = Arc::new(AtomicUsize::new(0));
800        let segments: Arc<[SegmentSpec]> = (0..4)
801            .map(|i| SegmentSpec {
802                offset: i * 4,
803                length: 4,
804                alignment: Alignment::none(),
805            })
806            .collect();
807        let metrics = DefaultMetricsRegistry::default();
808        let request_metrics = RequestMetrics::new(&metrics, vec![]);
809        let source = FileSegmentSource::open(
810            segments,
811            ReadRangesOnly {
812                calls: Arc::clone(&calls),
813            },
814            TokioRuntime::current(),
815            request_metrics.clone(),
816        );
817
818        let results = future::join_all((0..4).map(|i| source.request(SegmentId::from(i)))).await;
819
820        for result in results {
821            assert_eq!(result?.len(), 4);
822        }
823        assert_eq!(calls.load(Ordering::Relaxed), 1);
824        assert_eq!(request_metrics.read_ranges_calls.value(), 1);
825        assert_eq!(request_metrics.read_ranges_multi.value(), 1);
826        assert_eq!(request_metrics.read_ranges_num_ranges.count(), 1);
827        assert_eq!(request_metrics.read_ranges_num_ranges.total(), 4.0);
828        Ok(())
829    }
830
831    #[derive(Clone)]
832    struct ControlledReadRanges {
833        active: Arc<AtomicUsize>,
834        max_active: Arc<AtomicUsize>,
835        batch_sizes: Arc<Mutex<Vec<usize>>>,
836        permits: Arc<tokio::sync::Semaphore>,
837    }
838
839    impl VortexReadAt for ControlledReadRanges {
840        fn concurrency(&self) -> usize {
841            4
842        }
843
844        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
845            async { Ok(24) }.boxed()
846        }
847
848        fn read_at(
849            &self,
850            _offset: u64,
851            _length: usize,
852            _alignment: Alignment,
853        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
854            async { panic!("read_at should not be called") }.boxed()
855        }
856
857        fn read_ranges(&self, requests: Arc<[ReadAtRequest]>) -> ReadAtStream {
858            self.batch_sizes.lock().push(requests.len());
859            let active = self.active.fetch_add(requests.len(), Ordering::SeqCst) + requests.len();
860            self.max_active.fetch_max(active, Ordering::SeqCst);
861
862            let reads = requests
863                .iter()
864                .copied()
865                .map(|request| {
866                    let active = Arc::clone(&self.active);
867                    let permits = Arc::clone(&self.permits);
868                    async move {
869                        let Ok(permit) = permits.acquire_owned().await else {
870                            vortex_panic!("test semaphore unexpectedly closed");
871                        };
872                        permit.forget();
873                        active.fetch_sub(1, Ordering::SeqCst);
874                        let buffer = BufferHandle::new_host(
875                            ByteBuffer::from(vec![0; request.length]).aligned(request.alignment),
876                        );
877                        (request, Ok(buffer))
878                    }
879                })
880                .collect::<Vec<_>>();
881            futures::stream::iter(reads).buffer_unordered(4).boxed()
882        }
883    }
884
885    #[tokio::test]
886    async fn read_driver_refills_global_concurrency_across_batches() -> VortexResult<()> {
887        let active = Arc::new(AtomicUsize::new(0));
888        let max_active = Arc::new(AtomicUsize::new(0));
889        let batch_sizes = Arc::new(Mutex::new(Vec::new()));
890        let permits = Arc::new(tokio::sync::Semaphore::new(0));
891        let segments: Arc<[SegmentSpec]> = (0..6)
892            .map(|i| SegmentSpec {
893                offset: i * 4,
894                length: 4,
895                alignment: Alignment::none(),
896            })
897            .collect();
898        let metrics = DefaultMetricsRegistry::default();
899        let source = FileSegmentSource::open(
900            segments,
901            ControlledReadRanges {
902                active: Arc::clone(&active),
903                max_active: Arc::clone(&max_active),
904                batch_sizes: Arc::clone(&batch_sizes),
905                permits: Arc::clone(&permits),
906            },
907            TokioRuntime::current(),
908            RequestMetrics::new(&metrics, vec![]),
909        );
910        let reads = TokioRuntime::current().spawn(async move {
911            future::join_all((0..6).map(|i| source.request(SegmentId::from(i)))).await
912        });
913
914        assert!(
915            tokio::time::timeout(std::time::Duration::from_secs(1), async {
916                while active.load(Ordering::SeqCst) != 4 {
917                    tokio::task::yield_now().await;
918                }
919            })
920            .await
921            .is_ok()
922        );
923
924        permits.add_permits(1);
925        assert!(
926            tokio::time::timeout(std::time::Duration::from_secs(1), async {
927                while batch_sizes.lock().len() < 2 || active.load(Ordering::SeqCst) != 4 {
928                    tokio::task::yield_now().await;
929                }
930            })
931            .await
932            .is_ok()
933        );
934        assert_eq!(batch_sizes.lock().as_slice(), [4, 1]);
935        assert_eq!(max_active.load(Ordering::SeqCst), 4);
936
937        permits.add_permits(5);
938        for result in reads.await {
939            assert_eq!(result?.len(), 4);
940        }
941        assert_eq!(max_active.load(Ordering::SeqCst), 4);
942        Ok(())
943    }
944
945    #[tokio::test]
946    async fn read_driver_keeps_slots_full_while_a_straggler_is_in_flight() -> VortexResult<()> {
947        let active = Arc::new(AtomicUsize::new(0));
948        let max_active = Arc::new(AtomicUsize::new(0));
949        let batch_sizes = Arc::new(Mutex::new(Vec::new()));
950        let permits = Arc::new(tokio::sync::Semaphore::new(0));
951        let segments: Arc<[SegmentSpec]> = (0..8)
952            .map(|i| SegmentSpec {
953                offset: i * 4,
954                length: 4,
955                alignment: Alignment::none(),
956            })
957            .collect();
958        let metrics = DefaultMetricsRegistry::default();
959        let source = FileSegmentSource::open(
960            segments,
961            ControlledReadRanges {
962                active: Arc::clone(&active),
963                max_active: Arc::clone(&max_active),
964                batch_sizes: Arc::clone(&batch_sizes),
965                permits: Arc::clone(&permits),
966            },
967            TokioRuntime::current(),
968            RequestMetrics::new(&metrics, vec![]),
969        );
970        let reads = TokioRuntime::current().spawn(async move {
971            future::join_all((0..8).map(|i| source.request(SegmentId::from(i)))).await
972        });
973
974        wait_for_active_reads(&active, 4).await;
975
976        // Complete three reads while leaving one original read blocked as a straggler. Each freed
977        // slot must be refilled before the next completion; a batch-barrier implementation would
978        // instead fall from four active reads to one and submit no replacement work.
979        for expected_calls in 2..=4 {
980            permits.add_permits(1);
981            assert!(
982                tokio::time::timeout(std::time::Duration::from_secs(1), async {
983                    while batch_sizes.lock().len() < expected_calls
984                        || active.load(Ordering::SeqCst) != 4
985                    {
986                        tokio::task::yield_now().await;
987                    }
988                })
989                .await
990                .is_ok()
991            );
992        }
993
994        assert_eq!(batch_sizes.lock().as_slice(), [4, 1, 1, 1]);
995        assert_eq!(active.load(Ordering::SeqCst), 4);
996        assert_eq!(max_active.load(Ordering::SeqCst), 4);
997
998        permits.add_permits(5);
999        for result in reads.await {
1000            assert_eq!(result?.len(), 4);
1001        }
1002        Ok(())
1003    }
1004
1005    async fn wait_for_active_reads(active: &AtomicUsize, expected: usize) {
1006        assert!(
1007            tokio::time::timeout(std::time::Duration::from_secs(1), async {
1008                while active.load(Ordering::SeqCst) != expected {
1009                    tokio::task::yield_now().await;
1010                }
1011            })
1012            .await
1013            .is_ok()
1014        );
1015    }
1016
1017    #[derive(Clone)]
1018    struct SlowErrReadAt;
1019
1020    impl VortexReadAt for SlowErrReadAt {
1021        fn concurrency(&self) -> usize {
1022            4
1023        }
1024
1025        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
1026            async { Ok(1024) }.boxed()
1027        }
1028
1029        fn read_at(
1030            &self,
1031            offset: u64,
1032            _length: usize,
1033            _alignment: Alignment,
1034        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
1035            async move {
1036                // Stagger completions so some reads finish while others are still in flight.
1037                for _ in 0..(offset as usize % 5 + 1) {
1038                    tokio::task::yield_now().await;
1039                }
1040                vortex_bail!("slow read done")
1041            }
1042            .boxed()
1043        }
1044    }
1045
1046    // Many segment reads driven on separate tasks must all make progress and complete.
1047    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1048    async fn read_driver_concurrent_reads_make_progress() {
1049        let n = 16u32;
1050        let segments: Arc<[SegmentSpec]> = (0..n)
1051            .map(|i| SegmentSpec {
1052                offset: u64::from(i) * 4,
1053                length: 4,
1054                alignment: Alignment::none(),
1055            })
1056            .collect();
1057        let metrics = DefaultMetricsRegistry::default();
1058        let source = Arc::new(FileSegmentSource::open(
1059            segments,
1060            SlowErrReadAt,
1061            TokioRuntime::current(),
1062            RequestMetrics::new(&metrics, vec![]),
1063        ));
1064
1065        let handle = TokioRuntime::current();
1066        let tasks: Vec<_> = (0..n)
1067            .map(|i| {
1068                let source = Arc::clone(&source);
1069                handle.spawn(async move { source.request(SegmentId::from(i)).await })
1070            })
1071            .collect();
1072
1073        let joined =
1074            tokio::time::timeout(std::time::Duration::from_secs(5), future::join_all(tasks)).await;
1075
1076        let results = joined.expect("concurrent reads stalled before completing");
1077        assert_eq!(results.len(), n as usize);
1078        for result in results {
1079            let err = result.expect_err("reader must forward the I/O error");
1080            assert!(err.to_string().contains("slow read done"), "{err}");
1081        }
1082    }
1083}