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::future::Future;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::sync::atomic::AtomicUsize;
9use std::sync::atomic::Ordering;
10use std::task::Context;
11use std::task::Poll;
12
13use futures::FutureExt;
14use futures::StreamExt;
15use futures::channel::mpsc;
16use futures::future;
17use futures::future::BoxFuture;
18use futures::future::Shared;
19use parking_lot::Mutex;
20use vortex_array::buffer::BufferHandle;
21use vortex_buffer::Alignment;
22use vortex_buffer::ByteBuffer;
23use vortex_error::VortexResult;
24use vortex_error::vortex_bail;
25use vortex_error::vortex_err;
26use vortex_error::vortex_panic;
27use vortex_io::VortexReadAt;
28use vortex_io::runtime::Handle;
29use vortex_io::runtime::JoinOutcome;
30use vortex_layout::segments::SegmentFuture;
31use vortex_layout::segments::SegmentId;
32use vortex_layout::segments::SegmentSource;
33use vortex_metrics::Counter;
34use vortex_metrics::Histogram;
35use vortex_metrics::Label;
36use vortex_metrics::MetricBuilder;
37use vortex_metrics::MetricsRegistry;
38
39use crate::SegmentSpec;
40use crate::read::IoRequestStream;
41use crate::read::ReadRequest;
42use crate::read::RequestId;
43
44#[derive(Debug)]
45/// Events sent from segment futures to the coalescing read driver.
46pub enum ReadEvent {
47    /// A segment read has been registered.
48    Request(ReadRequest),
49    /// A registered read future has been polled.
50    Polled(RequestId),
51    /// A registered read future was dropped before completion.
52    Dropped(RequestId),
53}
54
55/// A [`SegmentSource`] for file-like IO.
56/// ## Coalescing and Pre-fetching
57///
58/// It is important to understand the semantics of the read futures returned by a [`FileSegmentSource`].
59/// Under the hood, each instance is backed by a stream that services read requests by
60/// applying coalescing and concurrency constraints.
61///
62/// Each read future has four states:
63/// * `registered` - the read future has been created, but not yet polled.
64/// * `requested` - the read future has been polled.
65/// * `in-flight` - the read request has been sent to the underlying storage system.
66/// * `resolved` - the read future has completed and resolved a result.
67///
68/// When a read request is `registered`, it will not itself trigger any I/O, but is eligible to
69/// be coalesced with other requests.
70///
71/// If a read future is dropped, it will be canceled if possible. This depends on the current
72/// state of the request, as well as whether the underlying storage system supports cancellation.
73///
74/// I/O requests will be processed in the order they are `registered`, however coalescing may mean
75/// other registered requests are lumped together into a single I/O operation.
76/// A cloneable handle to the background read driver, shared by every in-flight [`ReadFuture`].
77///
78/// [`Shared`] fans a single completion out to all readers with correct waker bookkeeping, so a
79/// reader is always woken when the driver finishes — even if another reader polled the driver more
80/// recently and was then dropped. Its output is `()`; a driver panic is carried out of band in
81/// [`DriverPanic`] so it can be re-raised on the reader side.
82type SharedDriver = Shared<BoxFuture<'static, ()>>;
83
84/// Slot holding the driver's panic payload, if it panicked while driving reads. The first reader to
85/// observe completion takes the payload and re-raises it; later readers report a graceful error.
86type DriverPanic = Arc<Mutex<Option<Box<dyn Any + Send>>>>;
87
88pub struct FileSegmentSource {
89    segments: Arc<[SegmentSpec]>,
90    /// A queue for sending read request events to the I/O stream.
91    events: mpsc::UnboundedSender<ReadEvent>,
92    /// Background request driver, joined by readers to surface a driver panic.
93    driver: SharedDriver,
94    /// Panic payload captured if the driver panicked while driving reads.
95    driver_panic: DriverPanic,
96    /// The next read request ID.
97    next_id: Arc<AtomicUsize>,
98}
99
100impl FileSegmentSource {
101    /// Open a file-backed segment source over `reader`.
102    ///
103    /// The returned source spawns a background driver on `handle` that coalesces and executes
104    /// random-access read requests.
105    pub fn open<R: VortexReadAt + Clone>(
106        segments: Arc<[SegmentSpec]>,
107        reader: R,
108        handle: Handle,
109        metrics: RequestMetrics,
110    ) -> Self {
111        let (send, recv) = mpsc::unbounded();
112
113        let max_alignment = segments
114            .iter()
115            .map(|segment| segment.alignment)
116            .max()
117            .unwrap_or_else(Alignment::none);
118        let coalesce_config = reader.coalesce_config().map(|mut config| {
119            // Aligning the coalesced start down can add up to (alignment - 1) bytes.
120            // Increase max_size to keep the effective payload window consistent.
121            let extra = (*max_alignment as u64).saturating_sub(1);
122            config.max_size = config.max_size.saturating_add(extra);
123            config
124        });
125        let concurrency = reader.concurrency();
126        if concurrency == 0 {
127            vortex_panic!(
128                "VortexReadAt::concurrency returned 0 (uri={:?}); this would stall I/O",
129                reader.uri()
130            );
131        }
132
133        let stream = IoRequestStream::new(
134            StreamExt::boxed(recv),
135            coalesce_config,
136            max_alignment,
137            metrics,
138        )
139        .boxed();
140
141        let drive_fut = async move {
142            stream
143                .map(move |req| {
144                    let reader = reader.clone();
145                    async move {
146                        let result = reader
147                            .read_at(req.offset(), req.len(), req.alignment())
148                            .await;
149                        let result = result.and_then(|buffer| {
150                            if req.len() != buffer.len() {
151                                vortex_bail!(
152                                    "FileSegmentSource: expected buffer of length {} but received {}. {:?}",
153                                    req.len(),
154                                    buffer.len(),
155                                    req
156                                )
157                            }
158                            Ok(buffer)
159                        });
160
161                        req.resolve(result);
162                    }
163                })
164                .buffer_unordered(concurrency)
165                .collect::<()>()
166                .await
167        };
168
169        // Spawn the driver so the runtime makes I/O progress independently of any reader. Readers
170        // join it (below) only to surface a panic raised while driving reads.
171        let mut task = handle.spawn(drive_fut);
172        let driver_panic: DriverPanic = Arc::new(Mutex::new(None));
173        let driver = {
174            let driver_panic = Arc::clone(&driver_panic);
175            async move {
176                // Poll for the terminal outcome without re-raising: a benign abort (runtime
177                // teardown) resolves to `()` so readers report a graceful error, while a panic is
178                // stashed for the first reader to re-raise.
179                if let JoinOutcome::Panicked(panic) = future::poll_fn(|cx| task.poll_join(cx)).await
180                {
181                    *driver_panic.lock() = Some(panic);
182                }
183            }
184            .boxed()
185            .shared()
186        };
187
188        Self {
189            segments,
190            events: send,
191            driver,
192            driver_panic,
193            next_id: Arc::new(AtomicUsize::new(0)),
194        }
195    }
196}
197
198impl SegmentSource for FileSegmentSource {
199    fn request(&self, id: SegmentId) -> SegmentFuture {
200        // We eagerly register the read request here assuming the behaviour of [`FileSegmentSource`], where
201        // coalescing becomes effective prior to the future being polled.
202        let spec = *match self.segments.get(*id as usize) {
203            Some(spec) => spec,
204            None => {
205                return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
206            }
207        };
208
209        let SegmentSpec {
210            offset,
211            length,
212            alignment,
213        } = spec;
214
215        let (send, recv) = oneshot::channel();
216        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
217        let event = ReadEvent::Request(ReadRequest {
218            id,
219            offset,
220            length: length as usize,
221            alignment,
222            callback: send,
223        });
224
225        // If we fail to submit the event, we create a future that has failed.
226        if let Err(e) = self.events.unbounded_send(event) {
227            return future::ready(Err(vortex_err!("Failed to submit read request: {e}"))).boxed();
228        }
229
230        let fut = ReadFuture {
231            id,
232            recv: recv.into_future(),
233            polled: false,
234            finished: false,
235            events: self.events.clone(),
236            driver: self.driver.clone(),
237            driver_panic: Arc::clone(&self.driver_panic),
238        };
239
240        // One allocation: we only box the returned SegmentFuture, not the inner ReadFuture.
241        fut.boxed()
242    }
243}
244
245/// A future that resolves a read request from a [`FileSegmentSource`].
246///
247/// See the documentation for [`FileSegmentSource`] for details on coalescing and pre-fetching.
248/// If dropped, the read request will be canceled where possible.
249struct ReadFuture {
250    id: usize,
251    recv: oneshot::AsyncReceiver<VortexResult<BufferHandle>>,
252    polled: bool,
253    finished: bool,
254    events: mpsc::UnboundedSender<ReadEvent>,
255    driver: SharedDriver,
256    driver_panic: DriverPanic,
257}
258
259impl Future for ReadFuture {
260    type Output = VortexResult<BufferHandle>;
261
262    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
263        match self.recv.poll_unpin(cx) {
264            // note: we are skipping polled and dropped events for this if the future is ready on
265            //       the first poll, that means this request was completed before it was polled,
266            //       as part of a coalesced request.
267            Poll::Ready(Ok(result)) => {
268                self.finished = true;
269                Poll::Ready(result)
270            }
271            // The request's sender was dropped, so the driver has finished. Join it so a panic
272            // raised while driving reads is re-raised here rather than surfacing as a generic
273            // error. Only report the dropped error once the driver has finished.
274            Poll::Ready(Err(e)) => match self.driver.poll_unpin(cx) {
275                Poll::Ready(()) => {
276                    self.finished = true;
277                    // Re-raise the driver panic on the first reader to observe it; later readers
278                    // fall through to the graceful dropped error.
279                    if let Some(panic) = self.driver_panic.lock().take() {
280                        std::panic::resume_unwind(panic);
281                    }
282                    Poll::Ready(Err(vortex_err!("ReadRequest dropped by runtime: {e}")))
283                }
284                Poll::Pending => Poll::Pending,
285            },
286            Poll::Pending if !self.polled => {
287                self.polled = true;
288                // Notify the I/O stream that this request has been polled.
289                match self.events.unbounded_send(ReadEvent::Polled(self.id)) {
290                    Ok(()) => Poll::Pending,
291                    Err(e) => Poll::Ready(Err(vortex_err!("ReadRequest dropped by runtime: {e}"))),
292                }
293            }
294            _ => Poll::Pending,
295        }
296    }
297}
298
299impl Drop for ReadFuture {
300    fn drop(&mut self) {
301        // Completed requests have already left driver state.
302        if self.finished {
303            return;
304        }
305
306        // Best-effort cancellation signal to the I/O stream.
307        drop(self.events.unbounded_send(ReadEvent::Dropped(self.id)));
308    }
309}
310
311/// Metrics emitted by the file segment request driver.
312pub struct RequestMetrics {
313    /// Number of individual segment requests observed by the driver.
314    pub individual_requests: Counter,
315    /// Number of physical reads after coalescing.
316    pub coalesced_requests: Counter,
317    /// Distribution of how many segment requests were merged into each physical read.
318    pub num_requests_coalesced: Histogram,
319}
320
321impl RequestMetrics {
322    /// Create request metrics in `metrics_registry` with shared labels.
323    pub fn new(metrics_registry: &dyn MetricsRegistry, labels: Vec<Label>) -> Self {
324        Self {
325            individual_requests: MetricBuilder::new(metrics_registry)
326                .add_labels(labels.clone())
327                .counter("io.requests.individual"),
328            coalesced_requests: MetricBuilder::new(metrics_registry)
329                .add_labels(labels.clone())
330                .counter("io.requests.coalesced"),
331            num_requests_coalesced: MetricBuilder::new(metrics_registry)
332                .add_labels(labels)
333                .histogram("io.requests.coalesced.num_coalesced"),
334        }
335    }
336}
337
338/// A [`SegmentSource`] that resolves segments synchronously from an
339/// in-memory [`ByteBuffer`].
340///
341/// Resolves segments synchronously, bypassing the async I/O pipeline.
342pub(crate) struct BufferSegmentSource {
343    buffer: ByteBuffer,
344    segments: Arc<[SegmentSpec]>,
345}
346
347impl BufferSegmentSource {
348    /// Create a new `BufferSegmentSource` from a buffer and its segment map.
349    pub fn new(buffer: ByteBuffer, segments: Arc<[SegmentSpec]>) -> Self {
350        Self { buffer, segments }
351    }
352}
353
354impl SegmentSource for BufferSegmentSource {
355    fn request(&self, id: SegmentId) -> SegmentFuture {
356        let spec = match self.segments.get(*id as usize) {
357            Some(spec) => spec,
358            None => {
359                return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
360            }
361        };
362
363        let start = spec.offset as usize;
364        let end = start + spec.length as usize;
365        if end > self.buffer.len() {
366            return future::ready(Err(vortex_err!(
367                "Segment {} range {}..{} out of bounds for buffer of length {}",
368                *id,
369                start,
370                end,
371                self.buffer.len()
372            )))
373            .boxed();
374        }
375
376        let slice = self.buffer.slice(start..end).aligned(spec.alignment);
377        future::ready(Ok(BufferHandle::new_host(slice))).boxed()
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use std::panic::AssertUnwindSafe;
384
385    use futures::future::BoxFuture;
386    use vortex_io::runtime::tokio::TokioRuntime;
387    use vortex_layout::segments::SegmentSource;
388    use vortex_metrics::DefaultMetricsRegistry;
389
390    use super::*;
391
392    #[derive(Clone)]
393    struct PanickingReadAt;
394
395    impl VortexReadAt for PanickingReadAt {
396        fn concurrency(&self) -> usize {
397            1
398        }
399
400        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
401            async { Ok(4) }.boxed()
402        }
403
404        fn read_at(
405            &self,
406            _offset: u64,
407            _length: usize,
408            _alignment: Alignment,
409        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
410            async {
411                panic!("read-at panic");
412            }
413            .boxed()
414        }
415    }
416
417    fn panicking_source() -> FileSegmentSource {
418        let segments: Arc<[SegmentSpec]> = Arc::from([SegmentSpec {
419            offset: 0,
420            length: 4,
421            alignment: Alignment::none(),
422        }]);
423        let metrics = DefaultMetricsRegistry::default();
424        FileSegmentSource::open(
425            segments,
426            PanickingReadAt,
427            TokioRuntime::current(),
428            RequestMetrics::new(&metrics, vec![]),
429        )
430    }
431
432    fn panic_message(payload: &(dyn Any + Send)) -> &str {
433        payload
434            .downcast_ref::<&str>()
435            .copied()
436            .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
437            .unwrap_or("<non-string panic>")
438    }
439
440    #[tokio::test]
441    #[should_panic(expected = "read-at panic")]
442    async fn file_segment_source_propagates_read_driver_panic() {
443        let source = panicking_source();
444        let _result = source.request(SegmentId::from(0)).await;
445    }
446
447    // A read-driver panic must propagate on *every* run rather than sometimes surfacing as a
448    // generic "dropped by runtime" error, which is nondeterministic.
449    #[tokio::test]
450    async fn file_segment_source_read_driver_panic_propagates_deterministically() {
451        for i in 0..100 {
452            let source = panicking_source();
453            let outcome = AssertUnwindSafe(source.request(SegmentId::from(0)))
454                .catch_unwind()
455                .await;
456            assert!(
457                outcome.is_err(),
458                "read-driver panic was not propagated on iteration {i}; \
459                 the request resolved instead of panicking"
460            );
461        }
462    }
463
464    // A driver panic must surface to every concurrent reader sharing one driver: exactly one
465    // reader re-raises the original panic, the rest report a graceful error, and none hang. This
466    // also covers the fan-out invariant — `Shared` wakes every reader on completion, so a reader
467    // dropped mid-flight can never swallow another reader's wake-up.
468    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
469    async fn file_segment_source_panic_propagates_to_all_concurrent_readers() {
470        let source = Arc::new(panicking_source());
471        let reader_count = 8;
472
473        let readers: Vec<_> = (0..reader_count)
474            .map(|_| {
475                let source = Arc::clone(&source);
476                tokio::spawn(async move {
477                    AssertUnwindSafe(source.request(SegmentId::from(0)))
478                        .catch_unwind()
479                        .await
480                })
481            })
482            .collect();
483
484        let joined =
485            tokio::time::timeout(std::time::Duration::from_secs(5), future::join_all(readers))
486                .await
487                .expect("a reader hung instead of observing the driver panic");
488
489        let mut original_panics = 0;
490        for reader in joined {
491            match reader.expect("reader task panicked at the join boundary") {
492                // The first reader to observe completion re-raises the original panic.
493                Err(payload) => {
494                    assert!(
495                        panic_message(&*payload).contains("read-at panic"),
496                        "got: {:?}",
497                        panic_message(&*payload)
498                    );
499                    original_panics += 1;
500                }
501                // Every other reader reports a graceful dropped-by-runtime error.
502                Ok(result) => assert!(result.is_err(), "expected a dropped-by-runtime error"),
503            }
504        }
505
506        assert_eq!(
507            original_panics, 1,
508            "exactly one reader should re-raise the original driver panic"
509        );
510    }
511
512    #[derive(Clone)]
513    struct SlowErrReadAt;
514
515    impl VortexReadAt for SlowErrReadAt {
516        fn concurrency(&self) -> usize {
517            4
518        }
519
520        fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
521            async { Ok(1024) }.boxed()
522        }
523
524        fn read_at(
525            &self,
526            offset: u64,
527            _length: usize,
528            _alignment: Alignment,
529        ) -> BoxFuture<'static, VortexResult<BufferHandle>> {
530            async move {
531                // Stagger completions so some reads finish while others are still in flight.
532                for _ in 0..(offset as usize % 5 + 1) {
533                    tokio::task::yield_now().await;
534                }
535                vortex_bail!("slow read done")
536            }
537            .boxed()
538        }
539    }
540
541    // Many segment reads driven on separate tasks must all make progress and complete.
542    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
543    async fn read_driver_concurrent_reads_make_progress() {
544        let n = 16u32;
545        let segments: Arc<[SegmentSpec]> = (0..n)
546            .map(|i| SegmentSpec {
547                offset: u64::from(i) * 4,
548                length: 4,
549                alignment: Alignment::none(),
550            })
551            .collect();
552        let metrics = DefaultMetricsRegistry::default();
553        let source = Arc::new(FileSegmentSource::open(
554            segments,
555            SlowErrReadAt,
556            TokioRuntime::current(),
557            RequestMetrics::new(&metrics, vec![]),
558        ));
559
560        let tasks: Vec<_> = (0..n)
561            .map(|i| {
562                let source = Arc::clone(&source);
563                tokio::spawn(async move { source.request(SegmentId::from(i)).await })
564            })
565            .collect();
566
567        let joined =
568            tokio::time::timeout(std::time::Duration::from_secs(5), future::join_all(tasks)).await;
569
570        assert!(joined.is_ok(), "concurrent reads stalled before completing");
571    }
572}