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
567    use futures::future::BoxFuture;
568    use vortex_error::vortex_bail;
569    use vortex_io::runtime::tokio::TokioRuntime;
570    use vortex_layout::segments::SegmentSource;
571    use vortex_metrics::DefaultMetricsRegistry;
572
573    use super::*;
574
575    fn io_request(id: RequestId, offset: u64, length: usize) -> IoRequest {
576        let (callback, _receiver) = oneshot::channel();
577        IoRequest::new_single(ReadRequest {
578            id,
579            offset,
580            length,
581            alignment: Alignment::none(),
582            callback,
583        })
584    }
585
586    #[tokio::test]
587    async fn read_range_results_matches_results_and_reports_missing_requests() {
588        let requests = vec![io_request(0, 0, 4), io_request(1, 4, 4)];
589        let unknown = ReadAtRequest::new(8, 4, Alignment::none());
590        let returned = ReadAtRequest::new(4, 4, Alignment::none());
591        let buffer = BufferHandle::new_host(ByteBuffer::from(vec![0; 4]));
592        let results =
593            futures::stream::iter([(unknown, Ok(buffer.clone())), (returned, Ok(buffer))]).boxed();
594
595        let resolved = ReadRangeResults::new(results, requests)
596            .collect::<Vec<_>>()
597            .await;
598
599        assert_eq!(resolved.len(), 2);
600        assert_eq!(resolved[0].0.offset(), 4);
601        assert!(resolved[0].1.is_ok());
602        assert_eq!(resolved[1].0.offset(), 0);
603        assert!(resolved[1].1.is_err());
604    }
605
606    #[derive(Clone)]
607    struct PanickingReadAt;
608
609    impl VortexReadAt for PanickingReadAt {
610        fn concurrency(&self) -> usize {
611            1
612        }
613
614        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
615            async { Ok(4) }.boxed()
616        }
617
618        fn read_at(
619            &self,
620            _offset: u64,
621            _length: usize,
622            _alignment: Alignment,
623        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
624            async {
625                panic!("read-at panic");
626            }
627            .boxed()
628        }
629    }
630
631    fn panicking_source() -> FileSegmentSource {
632        let segments: Arc<[SegmentSpec]> = Arc::from([SegmentSpec {
633            offset: 0,
634            length: 4,
635            alignment: Alignment::none(),
636        }]);
637        let metrics = DefaultMetricsRegistry::default();
638        FileSegmentSource::open(
639            segments,
640            PanickingReadAt,
641            TokioRuntime::current(),
642            RequestMetrics::new(&metrics, vec![]),
643        )
644    }
645
646    fn panic_message(payload: &(dyn Any + Send)) -> &str {
647        payload
648            .downcast_ref::<&str>()
649            .copied()
650            .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
651            .unwrap_or("<non-string panic>")
652    }
653
654    #[tokio::test]
655    #[should_panic(expected = "read-at panic")]
656    async fn file_segment_source_propagates_read_driver_panic() {
657        let source = panicking_source();
658        let _result = source.request(SegmentId::from(0)).await;
659    }
660
661    // A read-driver panic must propagate on *every* run rather than sometimes surfacing as a
662    // generic "dropped by runtime" error, which is nondeterministic.
663    #[tokio::test]
664    async fn file_segment_source_read_driver_panic_propagates_deterministically() {
665        for i in 0..100 {
666            let source = panicking_source();
667            let outcome = AssertUnwindSafe(source.request(SegmentId::from(0)))
668                .catch_unwind()
669                .await;
670            assert!(
671                outcome.is_err(),
672                "read-driver panic was not propagated on iteration {i}; \
673                 the request resolved instead of panicking"
674            );
675        }
676    }
677
678    // A driver panic must surface to every concurrent reader sharing one driver: exactly one
679    // reader re-raises the original panic, the rest report a graceful error, and none hang. This
680    // also covers the fan-out invariant — `Shared` wakes every reader on completion, so a reader
681    // dropped mid-flight can never swallow another reader's wake-up.
682    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
683    async fn file_segment_source_panic_propagates_to_all_concurrent_readers() {
684        let source = Arc::new(panicking_source());
685        let handle = TokioRuntime::current();
686        let reader_count = 8;
687
688        let readers: Vec<_> = (0..reader_count)
689            .map(|_| {
690                let source = Arc::clone(&source);
691                handle.spawn(async move {
692                    AssertUnwindSafe(source.request(SegmentId::from(0)))
693                        .catch_unwind()
694                        .await
695                })
696            })
697            .collect();
698
699        let joined =
700            tokio::time::timeout(std::time::Duration::from_secs(5), future::join_all(readers))
701                .await
702                .expect("a reader hung instead of observing the driver panic");
703
704        let mut original_panics = 0;
705        for reader in joined {
706            match reader {
707                // The first reader to observe completion re-raises the original panic.
708                Err(payload) => {
709                    assert!(
710                        panic_message(&*payload).contains("read-at panic"),
711                        "got: {:?}",
712                        panic_message(&*payload)
713                    );
714                    original_panics += 1;
715                }
716                // Every other reader reports a graceful dropped-by-runtime error.
717                Ok(result) => assert!(result.is_err(), "expected a dropped-by-runtime error"),
718            }
719        }
720
721        assert_eq!(
722            original_panics, 1,
723            "exactly one reader should re-raise the original driver panic"
724        );
725    }
726
727    #[derive(Clone)]
728    struct ReadRangesOnly {
729        calls: Arc<AtomicUsize>,
730    }
731
732    impl VortexReadAt for ReadRangesOnly {
733        fn concurrency(&self) -> usize {
734            4
735        }
736
737        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
738            async { Ok(16) }.boxed()
739        }
740
741        fn read_at(
742            &self,
743            _offset: u64,
744            _length: usize,
745            _alignment: Alignment,
746        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
747            async { panic!("read_at should not be called") }.boxed()
748        }
749
750        fn read_ranges(&self, requests: Arc<[ReadAtRequest]>) -> ReadAtStream {
751            self.calls.fetch_add(1, Ordering::Relaxed);
752            let results = requests
753                .iter()
754                .copied()
755                .map(|request| {
756                    let buffer = BufferHandle::new_host(
757                        ByteBuffer::from(vec![0; request.length]).aligned(request.alignment),
758                    );
759                    (request, Ok(buffer))
760                })
761                .collect::<Vec<_>>();
762            futures::stream::iter(results).boxed()
763        }
764    }
765
766    #[tokio::test]
767    async fn read_driver_batches_ready_requests() -> VortexResult<()> {
768        let calls = Arc::new(AtomicUsize::new(0));
769        let segments: Arc<[SegmentSpec]> = (0..4)
770            .map(|i| SegmentSpec {
771                offset: i * 4,
772                length: 4,
773                alignment: Alignment::none(),
774            })
775            .collect();
776        let metrics = DefaultMetricsRegistry::default();
777        let request_metrics = RequestMetrics::new(&metrics, vec![]);
778        let source = FileSegmentSource::open(
779            segments,
780            ReadRangesOnly {
781                calls: Arc::clone(&calls),
782            },
783            TokioRuntime::current(),
784            request_metrics.clone(),
785        );
786
787        let results = future::join_all((0..4).map(|i| source.request(SegmentId::from(i)))).await;
788
789        for result in results {
790            assert_eq!(result?.len(), 4);
791        }
792        assert_eq!(calls.load(Ordering::Relaxed), 1);
793        assert_eq!(request_metrics.read_ranges_calls.value(), 1);
794        assert_eq!(request_metrics.read_ranges_multi.value(), 1);
795        assert_eq!(request_metrics.read_ranges_num_ranges.count(), 1);
796        assert_eq!(request_metrics.read_ranges_num_ranges.total(), 4.0);
797        Ok(())
798    }
799
800    #[derive(Clone)]
801    struct ControlledReadRanges {
802        active: Arc<AtomicUsize>,
803        max_active: Arc<AtomicUsize>,
804        batch_sizes: Arc<Mutex<Vec<usize>>>,
805        permits: Arc<tokio::sync::Semaphore>,
806    }
807
808    impl VortexReadAt for ControlledReadRanges {
809        fn concurrency(&self) -> usize {
810            4
811        }
812
813        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
814            async { Ok(24) }.boxed()
815        }
816
817        fn read_at(
818            &self,
819            _offset: u64,
820            _length: usize,
821            _alignment: Alignment,
822        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
823            async { panic!("read_at should not be called") }.boxed()
824        }
825
826        fn read_ranges(&self, requests: Arc<[ReadAtRequest]>) -> ReadAtStream {
827            self.batch_sizes.lock().push(requests.len());
828            let active = self.active.fetch_add(requests.len(), Ordering::SeqCst) + requests.len();
829            self.max_active.fetch_max(active, Ordering::SeqCst);
830
831            let reads = requests
832                .iter()
833                .copied()
834                .map(|request| {
835                    let active = Arc::clone(&self.active);
836                    let permits = Arc::clone(&self.permits);
837                    async move {
838                        let Ok(permit) = permits.acquire_owned().await else {
839                            vortex_panic!("test semaphore unexpectedly closed");
840                        };
841                        permit.forget();
842                        active.fetch_sub(1, Ordering::SeqCst);
843                        let buffer = BufferHandle::new_host(
844                            ByteBuffer::from(vec![0; request.length]).aligned(request.alignment),
845                        );
846                        (request, Ok(buffer))
847                    }
848                })
849                .collect::<Vec<_>>();
850            futures::stream::iter(reads).buffer_unordered(4).boxed()
851        }
852    }
853
854    #[tokio::test]
855    async fn read_driver_refills_global_concurrency_across_batches() -> VortexResult<()> {
856        let active = Arc::new(AtomicUsize::new(0));
857        let max_active = Arc::new(AtomicUsize::new(0));
858        let batch_sizes = Arc::new(Mutex::new(Vec::new()));
859        let permits = Arc::new(tokio::sync::Semaphore::new(0));
860        let segments: Arc<[SegmentSpec]> = (0..6)
861            .map(|i| SegmentSpec {
862                offset: i * 4,
863                length: 4,
864                alignment: Alignment::none(),
865            })
866            .collect();
867        let metrics = DefaultMetricsRegistry::default();
868        let source = FileSegmentSource::open(
869            segments,
870            ControlledReadRanges {
871                active: Arc::clone(&active),
872                max_active: Arc::clone(&max_active),
873                batch_sizes: Arc::clone(&batch_sizes),
874                permits: Arc::clone(&permits),
875            },
876            TokioRuntime::current(),
877            RequestMetrics::new(&metrics, vec![]),
878        );
879        let reads = TokioRuntime::current().spawn(async move {
880            future::join_all((0..6).map(|i| source.request(SegmentId::from(i)))).await
881        });
882
883        assert!(
884            tokio::time::timeout(std::time::Duration::from_secs(1), async {
885                while active.load(Ordering::SeqCst) != 4 {
886                    tokio::task::yield_now().await;
887                }
888            })
889            .await
890            .is_ok()
891        );
892
893        permits.add_permits(1);
894        assert!(
895            tokio::time::timeout(std::time::Duration::from_secs(1), async {
896                while batch_sizes.lock().len() < 2 || active.load(Ordering::SeqCst) != 4 {
897                    tokio::task::yield_now().await;
898                }
899            })
900            .await
901            .is_ok()
902        );
903        assert_eq!(batch_sizes.lock().as_slice(), [4, 1]);
904        assert_eq!(max_active.load(Ordering::SeqCst), 4);
905
906        permits.add_permits(5);
907        for result in reads.await {
908            assert_eq!(result?.len(), 4);
909        }
910        assert_eq!(max_active.load(Ordering::SeqCst), 4);
911        Ok(())
912    }
913
914    #[tokio::test]
915    async fn read_driver_keeps_slots_full_while_a_straggler_is_in_flight() -> VortexResult<()> {
916        let active = Arc::new(AtomicUsize::new(0));
917        let max_active = Arc::new(AtomicUsize::new(0));
918        let batch_sizes = Arc::new(Mutex::new(Vec::new()));
919        let permits = Arc::new(tokio::sync::Semaphore::new(0));
920        let segments: Arc<[SegmentSpec]> = (0..8)
921            .map(|i| SegmentSpec {
922                offset: i * 4,
923                length: 4,
924                alignment: Alignment::none(),
925            })
926            .collect();
927        let metrics = DefaultMetricsRegistry::default();
928        let source = FileSegmentSource::open(
929            segments,
930            ControlledReadRanges {
931                active: Arc::clone(&active),
932                max_active: Arc::clone(&max_active),
933                batch_sizes: Arc::clone(&batch_sizes),
934                permits: Arc::clone(&permits),
935            },
936            TokioRuntime::current(),
937            RequestMetrics::new(&metrics, vec![]),
938        );
939        let reads = TokioRuntime::current().spawn(async move {
940            future::join_all((0..8).map(|i| source.request(SegmentId::from(i)))).await
941        });
942
943        wait_for_active_reads(&active, 4).await;
944
945        // Complete three reads while leaving one original read blocked as a straggler. Each freed
946        // slot must be refilled before the next completion; a batch-barrier implementation would
947        // instead fall from four active reads to one and submit no replacement work.
948        for expected_calls in 2..=4 {
949            permits.add_permits(1);
950            assert!(
951                tokio::time::timeout(std::time::Duration::from_secs(1), async {
952                    while batch_sizes.lock().len() < expected_calls
953                        || active.load(Ordering::SeqCst) != 4
954                    {
955                        tokio::task::yield_now().await;
956                    }
957                })
958                .await
959                .is_ok()
960            );
961        }
962
963        assert_eq!(batch_sizes.lock().as_slice(), [4, 1, 1, 1]);
964        assert_eq!(active.load(Ordering::SeqCst), 4);
965        assert_eq!(max_active.load(Ordering::SeqCst), 4);
966
967        permits.add_permits(5);
968        for result in reads.await {
969            assert_eq!(result?.len(), 4);
970        }
971        Ok(())
972    }
973
974    async fn wait_for_active_reads(active: &AtomicUsize, expected: usize) {
975        assert!(
976            tokio::time::timeout(std::time::Duration::from_secs(1), async {
977                while active.load(Ordering::SeqCst) != expected {
978                    tokio::task::yield_now().await;
979                }
980            })
981            .await
982            .is_ok()
983        );
984    }
985
986    #[derive(Clone)]
987    struct SlowErrReadAt;
988
989    impl VortexReadAt for SlowErrReadAt {
990        fn concurrency(&self) -> usize {
991            4
992        }
993
994        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
995            async { Ok(1024) }.boxed()
996        }
997
998        fn read_at(
999            &self,
1000            offset: u64,
1001            _length: usize,
1002            _alignment: Alignment,
1003        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
1004            async move {
1005                // Stagger completions so some reads finish while others are still in flight.
1006                for _ in 0..(offset as usize % 5 + 1) {
1007                    tokio::task::yield_now().await;
1008                }
1009                vortex_bail!("slow read done")
1010            }
1011            .boxed()
1012        }
1013    }
1014
1015    // Many segment reads driven on separate tasks must all make progress and complete.
1016    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1017    async fn read_driver_concurrent_reads_make_progress() {
1018        let n = 16u32;
1019        let segments: Arc<[SegmentSpec]> = (0..n)
1020            .map(|i| SegmentSpec {
1021                offset: u64::from(i) * 4,
1022                length: 4,
1023                alignment: Alignment::none(),
1024            })
1025            .collect();
1026        let metrics = DefaultMetricsRegistry::default();
1027        let source = Arc::new(FileSegmentSource::open(
1028            segments,
1029            SlowErrReadAt,
1030            TokioRuntime::current(),
1031            RequestMetrics::new(&metrics, vec![]),
1032        ));
1033
1034        let handle = TokioRuntime::current();
1035        let tasks: Vec<_> = (0..n)
1036            .map(|i| {
1037                let source = Arc::clone(&source);
1038                handle.spawn(async move { source.request(SegmentId::from(i)).await })
1039            })
1040            .collect();
1041
1042        let joined =
1043            tokio::time::timeout(std::time::Duration::from_secs(5), future::join_all(tasks)).await;
1044
1045        let results = joined.expect("concurrent reads stalled before completing");
1046        assert_eq!(results.len(), n as usize);
1047        for result in results {
1048            let err = result.expect_err("reader must forward the I/O error");
1049            assert!(err.to_string().contains("slow read done"), "{err}");
1050        }
1051    }
1052}