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