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::pin::Pin;
5use std::sync::Arc;
6use std::sync::atomic::AtomicUsize;
7use std::sync::atomic::Ordering;
8use std::task::Context;
9use std::task::Poll;
10
11use futures::FutureExt;
12use futures::StreamExt;
13use futures::channel::mpsc;
14use futures::future;
15use vortex_array::buffer::BufferHandle;
16use vortex_buffer::Alignment;
17use vortex_buffer::ByteBuffer;
18use vortex_error::VortexResult;
19use vortex_error::vortex_bail;
20use vortex_error::vortex_err;
21use vortex_error::vortex_panic;
22use vortex_io::VortexReadAt;
23use vortex_io::runtime::Handle;
24use vortex_layout::segments::SegmentFuture;
25use vortex_layout::segments::SegmentId;
26use vortex_layout::segments::SegmentSource;
27use vortex_metrics::Counter;
28use vortex_metrics::Histogram;
29use vortex_metrics::Label;
30use vortex_metrics::MetricBuilder;
31use vortex_metrics::MetricsRegistry;
32
33use crate::SegmentSpec;
34use crate::read::IoRequestStream;
35use crate::read::ReadRequest;
36use crate::read::RequestId;
37
38#[derive(Debug)]
39/// Events sent from segment futures to the coalescing read driver.
40pub enum ReadEvent {
41    /// A segment read has been registered.
42    Request(ReadRequest),
43    /// A registered read future has been polled.
44    Polled(RequestId),
45    /// A registered read future was dropped before completion.
46    Dropped(RequestId),
47}
48
49/// A [`SegmentSource`] for file-like IO.
50/// ## Coalescing and Pre-fetching
51///
52/// It is important to understand the semantics of the read futures returned by a [`FileSegmentSource`].
53/// Under the hood, each instance is backed by a stream that services read requests by
54/// applying coalescing and concurrency constraints.
55///
56/// Each read future has four states:
57/// * `registered` - the read future has been created, but not yet polled.
58/// * `requested` - the read future has been polled.
59/// * `in-flight` - the read request has been sent to the underlying storage system.
60/// * `resolved` - the read future has completed and resolved a result.
61///
62/// When a read request is `registered`, it will not itself trigger any I/O, but is eligible to
63/// be coalesced with other requests.
64///
65/// If a read future is dropped, it will be canceled if possible. This depends on the current
66/// state of the request, as well as whether the underlying storage system supports cancellation.
67///
68/// I/O requests will be processed in the order they are `registered`, however coalescing may mean
69/// other registered requests are lumped together into a single I/O operation.
70pub struct FileSegmentSource {
71    segments: Arc<[SegmentSpec]>,
72    /// A queue for sending read request events to the I/O stream.
73    events: mpsc::UnboundedSender<ReadEvent>,
74    /// The next read request ID.
75    next_id: Arc<AtomicUsize>,
76}
77
78impl FileSegmentSource {
79    /// Open a file-backed segment source over `reader`.
80    ///
81    /// The returned source spawns a background driver on `handle` that coalesces and executes
82    /// random-access read requests.
83    pub fn open<R: VortexReadAt + Clone>(
84        segments: Arc<[SegmentSpec]>,
85        reader: R,
86        handle: Handle,
87        metrics: RequestMetrics,
88    ) -> Self {
89        let (send, recv) = mpsc::unbounded();
90
91        let max_alignment = segments
92            .iter()
93            .map(|segment| segment.alignment)
94            .max()
95            .unwrap_or_else(Alignment::none);
96        let coalesce_config = reader.coalesce_config().map(|mut config| {
97            // Aligning the coalesced start down can add up to (alignment - 1) bytes.
98            // Increase max_size to keep the effective payload window consistent.
99            let extra = (*max_alignment as u64).saturating_sub(1);
100            config.max_size = config.max_size.saturating_add(extra);
101            config
102        });
103        let concurrency = reader.concurrency();
104        if concurrency == 0 {
105            vortex_panic!(
106                "VortexReadAt::concurrency returned 0 (uri={:?}); this would stall I/O",
107                reader.uri()
108            );
109        }
110
111        let stream = IoRequestStream::new(
112            StreamExt::boxed(recv),
113            coalesce_config,
114            max_alignment,
115            metrics,
116        )
117        .boxed();
118
119        let drive_fut = async move {
120            stream
121                .map(move |req| {
122                    let reader = reader.clone();
123                    async move {
124                        let result = reader
125                            .read_at(req.offset(), req.len(), req.alignment())
126                            .await;
127                        let result = result.and_then(|buffer| {
128                            if req.len() != buffer.len() {
129                                vortex_bail!(
130                                    "FileSegmentSource: expected buffer of length {} but received {}. {:?}",
131                                    req.len(),
132                                    buffer.len(),
133                                    req
134                                )
135                            }
136                            Ok(buffer)
137                        });
138
139                        req.resolve(result);
140                    }
141                })
142                .buffer_unordered(concurrency)
143                .collect::<()>()
144                .await
145        };
146
147        handle.spawn(drive_fut).detach();
148
149        Self {
150            segments,
151            events: send,
152            next_id: Arc::new(AtomicUsize::new(0)),
153        }
154    }
155}
156
157impl SegmentSource for FileSegmentSource {
158    fn request(&self, id: SegmentId) -> SegmentFuture {
159        // We eagerly register the read request here assuming the behaviour of [`FileSegmentSource`], where
160        // coalescing becomes effective prior to the future being polled.
161        let spec = *match self.segments.get(*id as usize) {
162            Some(spec) => spec,
163            None => {
164                return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
165            }
166        };
167
168        let SegmentSpec {
169            offset,
170            length,
171            alignment,
172        } = spec;
173
174        let (send, recv) = oneshot::channel();
175        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
176        let event = ReadEvent::Request(ReadRequest {
177            id,
178            offset,
179            length: length as usize,
180            alignment,
181            callback: send,
182        });
183
184        // If we fail to submit the event, we create a future that has failed.
185        if let Err(e) = self.events.unbounded_send(event) {
186            return future::ready(Err(vortex_err!("Failed to submit read request: {e}"))).boxed();
187        }
188
189        let fut = ReadFuture {
190            id,
191            recv: recv.into_future(),
192            polled: false,
193            finished: false,
194            events: self.events.clone(),
195        };
196
197        // One allocation: we only box the returned SegmentFuture, not the inner ReadFuture.
198        fut.boxed()
199    }
200}
201
202/// A future that resolves a read request from a [`FileSegmentSource`].
203///
204/// See the documentation for [`FileSegmentSource`] for details on coalescing and pre-fetching.
205/// If dropped, the read request will be canceled where possible.
206struct ReadFuture {
207    id: usize,
208    recv: oneshot::AsyncReceiver<VortexResult<BufferHandle>>,
209    polled: bool,
210    finished: bool,
211    events: mpsc::UnboundedSender<ReadEvent>,
212}
213
214impl Future for ReadFuture {
215    type Output = VortexResult<BufferHandle>;
216
217    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
218        match self.recv.poll_unpin(cx) {
219            Poll::Ready(result) => {
220                self.finished = true;
221                // note: we are skipping polled and dropped events for this if the future
222                //       is ready on the first poll, that means this request was completed
223                //       before it was polled, as part of a coalesced request.
224                Poll::Ready(
225                    result.unwrap_or_else(|e| {
226                        Err(vortex_err!("ReadRequest dropped by runtime: {e}"))
227                    }),
228                )
229            }
230            Poll::Pending if !self.polled => {
231                self.polled = true;
232                // Notify the I/O stream that this request has been polled.
233                match self.events.unbounded_send(ReadEvent::Polled(self.id)) {
234                    Ok(()) => Poll::Pending,
235                    Err(e) => Poll::Ready(Err(vortex_err!("ReadRequest dropped by runtime: {e}"))),
236                }
237            }
238            _ => Poll::Pending,
239        }
240    }
241}
242
243impl Drop for ReadFuture {
244    fn drop(&mut self) {
245        // Completed requests have already left driver state.
246        if self.finished {
247            return;
248        }
249
250        // Best-effort cancellation signal to the I/O stream.
251        drop(self.events.unbounded_send(ReadEvent::Dropped(self.id)));
252    }
253}
254
255/// Metrics emitted by the file segment request driver.
256pub struct RequestMetrics {
257    /// Number of individual segment requests observed by the driver.
258    pub individual_requests: Counter,
259    /// Number of physical reads after coalescing.
260    pub coalesced_requests: Counter,
261    /// Distribution of how many segment requests were merged into each physical read.
262    pub num_requests_coalesced: Histogram,
263}
264
265impl RequestMetrics {
266    /// Create request metrics in `metrics_registry` with shared labels.
267    pub fn new(metrics_registry: &dyn MetricsRegistry, labels: Vec<Label>) -> Self {
268        Self {
269            individual_requests: MetricBuilder::new(metrics_registry)
270                .add_labels(labels.clone())
271                .counter("io.requests.individual"),
272            coalesced_requests: MetricBuilder::new(metrics_registry)
273                .add_labels(labels.clone())
274                .counter("io.requests.coalesced"),
275            num_requests_coalesced: MetricBuilder::new(metrics_registry)
276                .add_labels(labels)
277                .histogram("io.requests.coalesced.num_coalesced"),
278        }
279    }
280}
281
282/// A [`SegmentSource`] that resolves segments synchronously from an
283/// in-memory [`ByteBuffer`].
284///
285/// Resolves segments synchronously, bypassing the async I/O pipeline.
286pub(crate) struct BufferSegmentSource {
287    buffer: ByteBuffer,
288    segments: Arc<[SegmentSpec]>,
289}
290
291impl BufferSegmentSource {
292    /// Create a new `BufferSegmentSource` from a buffer and its segment map.
293    pub fn new(buffer: ByteBuffer, segments: Arc<[SegmentSpec]>) -> Self {
294        Self { buffer, segments }
295    }
296}
297
298impl SegmentSource for BufferSegmentSource {
299    fn request(&self, id: SegmentId) -> SegmentFuture {
300        let spec = match self.segments.get(*id as usize) {
301            Some(spec) => spec,
302            None => {
303                return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
304            }
305        };
306
307        let start = spec.offset as usize;
308        let end = start + spec.length as usize;
309        if end > self.buffer.len() {
310            return future::ready(Err(vortex_err!(
311                "Segment {} range {}..{} out of bounds for buffer of length {}",
312                *id,
313                start,
314                end,
315                self.buffer.len()
316            )))
317            .boxed();
318        }
319
320        let slice = self.buffer.slice(start..end).aligned(spec.alignment);
321        future::ready(Ok(BufferHandle::new_host(slice))).boxed()
322    }
323}