1use 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)]
45pub enum ReadEvent {
47 Request(ReadRequest),
49 Polled(RequestId),
51 Dropped(RequestId),
53}
54
55type SharedDriver = Shared<BoxFuture<'static, ()>>;
83
84type DriverPanic = Arc<Mutex<Option<Box<dyn Any + Send>>>>;
87
88pub struct FileSegmentSource {
89 segments: Arc<[SegmentSpec]>,
90 events: mpsc::UnboundedSender<ReadEvent>,
92 driver: SharedDriver,
94 driver_panic: DriverPanic,
96 next_id: Arc<AtomicUsize>,
98}
99
100impl FileSegmentSource {
101 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 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 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 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 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 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 fut.boxed()
242 }
243}
244
245struct 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 Poll::Ready(Ok(result)) => {
268 self.finished = true;
269 Poll::Ready(result)
270 }
271 Poll::Ready(Err(e)) => match self.driver.poll_unpin(cx) {
275 Poll::Ready(()) => {
276 self.finished = true;
277 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 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 if self.finished {
303 return;
304 }
305
306 drop(self.events.unbounded_send(ReadEvent::Dropped(self.id)));
308 }
309}
310
311pub struct RequestMetrics {
313 pub individual_requests: Counter,
315 pub coalesced_requests: Counter,
317 pub num_requests_coalesced: Histogram,
319}
320
321impl RequestMetrics {
322 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
338pub(crate) struct BufferSegmentSource {
343 buffer: ByteBuffer,
344 segments: Arc<[SegmentSpec]>,
345}
346
347impl BufferSegmentSource {
348 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 #[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 #[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 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 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 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 #[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}