use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::task::Context;
use std::task::Poll;
use futures::FutureExt;
use futures::StreamExt;
use futures::channel::mpsc;
use futures::future;
use futures::future::BoxFuture;
use futures::future::Shared;
use parking_lot::Mutex;
use vortex_array::buffer::BufferHandle;
use vortex_buffer::Alignment;
use vortex_buffer::ByteBuffer;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_error::vortex_err;
use vortex_error::vortex_panic;
use vortex_io::VortexReadAt;
use vortex_io::runtime::Handle;
use vortex_io::runtime::JoinOutcome;
use vortex_layout::segments::SegmentFuture;
use vortex_layout::segments::SegmentId;
use vortex_layout::segments::SegmentSource;
use vortex_metrics::Counter;
use vortex_metrics::Histogram;
use vortex_metrics::Label;
use vortex_metrics::MetricBuilder;
use vortex_metrics::MetricsRegistry;
use crate::SegmentSpec;
use crate::read::IoRequestStream;
use crate::read::ReadRequest;
use crate::read::RequestId;
#[derive(Debug)]
pub enum ReadEvent {
Request(ReadRequest),
Polled(RequestId),
Dropped(RequestId),
}
type SharedDriver = Shared<BoxFuture<'static, ()>>;
type DriverPanic = Arc<Mutex<Option<Box<dyn Any + Send>>>>;
pub struct FileSegmentSource {
segments: Arc<[SegmentSpec]>,
events: mpsc::UnboundedSender<ReadEvent>,
driver: SharedDriver,
driver_panic: DriverPanic,
next_id: Arc<AtomicUsize>,
}
impl FileSegmentSource {
pub fn open<R: VortexReadAt + Clone>(
segments: Arc<[SegmentSpec]>,
reader: R,
handle: Handle,
metrics: RequestMetrics,
) -> Self {
let (send, recv) = mpsc::unbounded();
let max_alignment = segments
.iter()
.map(|segment| segment.alignment)
.max()
.unwrap_or_else(Alignment::none);
let coalesce_config = reader.coalesce_config().map(|mut config| {
let extra = (*max_alignment as u64).saturating_sub(1);
config.max_size = config.max_size.saturating_add(extra);
config
});
let concurrency = reader.concurrency();
if concurrency == 0 {
vortex_panic!(
"VortexReadAt::concurrency returned 0 (uri={:?}); this would stall I/O",
reader.uri()
);
}
let stream = IoRequestStream::new(
StreamExt::boxed(recv),
coalesce_config,
max_alignment,
metrics,
)
.boxed();
let drive_fut = async move {
stream
.map(move |req| {
let reader = reader.clone();
async move {
let result = reader
.read_at(req.offset(), req.len(), req.alignment())
.await;
let result = result.and_then(|buffer| {
if req.len() != buffer.len() {
vortex_bail!(
"FileSegmentSource: expected buffer of length {} but received {}. {:?}",
req.len(),
buffer.len(),
req
)
}
Ok(buffer)
});
req.resolve(result);
}
})
.buffer_unordered(concurrency)
.collect::<()>()
.await
};
let mut task = handle.spawn(drive_fut);
let driver_panic: DriverPanic = Arc::new(Mutex::new(None));
let driver = {
let driver_panic = Arc::clone(&driver_panic);
async move {
if let JoinOutcome::Panicked(panic) = future::poll_fn(|cx| task.poll_join(cx)).await
{
*driver_panic.lock() = Some(panic);
}
}
.boxed()
.shared()
};
Self {
segments,
events: send,
driver,
driver_panic,
next_id: Arc::new(AtomicUsize::new(0)),
}
}
}
impl SegmentSource for FileSegmentSource {
fn request(&self, id: SegmentId) -> SegmentFuture {
let spec = *match self.segments.get(*id as usize) {
Some(spec) => spec,
None => {
return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
}
};
let SegmentSpec {
offset,
length,
alignment,
} = spec;
let (send, recv) = oneshot::channel();
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let event = ReadEvent::Request(ReadRequest {
id,
offset,
length: length as usize,
alignment,
callback: send,
});
if let Err(e) = self.events.unbounded_send(event) {
return future::ready(Err(vortex_err!("Failed to submit read request: {e}"))).boxed();
}
let fut = ReadFuture {
id,
recv: recv.into_future(),
polled: false,
finished: false,
events: self.events.clone(),
driver: self.driver.clone(),
driver_panic: Arc::clone(&self.driver_panic),
};
fut.boxed()
}
}
struct ReadFuture {
id: usize,
recv: oneshot::AsyncReceiver<VortexResult<BufferHandle>>,
polled: bool,
finished: bool,
events: mpsc::UnboundedSender<ReadEvent>,
driver: SharedDriver,
driver_panic: DriverPanic,
}
impl Future for ReadFuture {
type Output = VortexResult<BufferHandle>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.recv.poll_unpin(cx) {
Poll::Ready(Ok(result)) => {
self.finished = true;
Poll::Ready(result)
}
Poll::Ready(Err(e)) => match self.driver.poll_unpin(cx) {
Poll::Ready(()) => {
self.finished = true;
if let Some(panic) = self.driver_panic.lock().take() {
std::panic::resume_unwind(panic);
}
Poll::Ready(Err(vortex_err!("ReadRequest dropped by runtime: {e}")))
}
Poll::Pending => Poll::Pending,
},
Poll::Pending if !self.polled => {
self.polled = true;
match self.events.unbounded_send(ReadEvent::Polled(self.id)) {
Ok(()) => Poll::Pending,
Err(e) => Poll::Ready(Err(vortex_err!("ReadRequest dropped by runtime: {e}"))),
}
}
_ => Poll::Pending,
}
}
}
impl Drop for ReadFuture {
fn drop(&mut self) {
if self.finished {
return;
}
drop(self.events.unbounded_send(ReadEvent::Dropped(self.id)));
}
}
pub struct RequestMetrics {
pub individual_requests: Counter,
pub coalesced_requests: Counter,
pub num_requests_coalesced: Histogram,
}
impl RequestMetrics {
pub fn new(metrics_registry: &dyn MetricsRegistry, labels: Vec<Label>) -> Self {
Self {
individual_requests: MetricBuilder::new(metrics_registry)
.add_labels(labels.clone())
.counter("io.requests.individual"),
coalesced_requests: MetricBuilder::new(metrics_registry)
.add_labels(labels.clone())
.counter("io.requests.coalesced"),
num_requests_coalesced: MetricBuilder::new(metrics_registry)
.add_labels(labels)
.histogram("io.requests.coalesced.num_coalesced"),
}
}
}
pub(crate) struct BufferSegmentSource {
buffer: ByteBuffer,
segments: Arc<[SegmentSpec]>,
}
impl BufferSegmentSource {
pub fn new(buffer: ByteBuffer, segments: Arc<[SegmentSpec]>) -> Self {
Self { buffer, segments }
}
}
impl SegmentSource for BufferSegmentSource {
fn request(&self, id: SegmentId) -> SegmentFuture {
let spec = match self.segments.get(*id as usize) {
Some(spec) => spec,
None => {
return future::ready(Err(vortex_err!("Missing segment: {}", id))).boxed();
}
};
let start = spec.offset as usize;
let end = start + spec.length as usize;
if end > self.buffer.len() {
return future::ready(Err(vortex_err!(
"Segment {} range {}..{} out of bounds for buffer of length {}",
*id,
start,
end,
self.buffer.len()
)))
.boxed();
}
let slice = self.buffer.slice(start..end).aligned(spec.alignment);
future::ready(Ok(BufferHandle::new_host(slice))).boxed()
}
}
#[cfg(test)]
mod tests {
use std::panic::AssertUnwindSafe;
use futures::future::BoxFuture;
use vortex_io::runtime::tokio::TokioRuntime;
use vortex_layout::segments::SegmentSource;
use vortex_metrics::DefaultMetricsRegistry;
use super::*;
#[derive(Clone)]
struct PanickingReadAt;
impl VortexReadAt for PanickingReadAt {
fn concurrency(&self) -> usize {
1
}
fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
async { Ok(4) }.boxed()
}
fn read_at(
&self,
_offset: u64,
_length: usize,
_alignment: Alignment,
) -> BoxFuture<'static, VortexResult<BufferHandle>> {
async {
panic!("read-at panic");
}
.boxed()
}
}
fn panicking_source() -> FileSegmentSource {
let segments: Arc<[SegmentSpec]> = Arc::from([SegmentSpec {
offset: 0,
length: 4,
alignment: Alignment::none(),
}]);
let metrics = DefaultMetricsRegistry::default();
FileSegmentSource::open(
segments,
PanickingReadAt,
TokioRuntime::current(),
RequestMetrics::new(&metrics, vec![]),
)
}
fn panic_message(payload: &(dyn Any + Send)) -> &str {
payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("<non-string panic>")
}
#[tokio::test]
#[should_panic(expected = "read-at panic")]
async fn file_segment_source_propagates_read_driver_panic() {
let source = panicking_source();
let _result = source.request(SegmentId::from(0)).await;
}
#[tokio::test]
async fn file_segment_source_read_driver_panic_propagates_deterministically() {
for i in 0..100 {
let source = panicking_source();
let outcome = AssertUnwindSafe(source.request(SegmentId::from(0)))
.catch_unwind()
.await;
assert!(
outcome.is_err(),
"read-driver panic was not propagated on iteration {i}; \
the request resolved instead of panicking"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn file_segment_source_panic_propagates_to_all_concurrent_readers() {
let source = Arc::new(panicking_source());
let reader_count = 8;
let readers: Vec<_> = (0..reader_count)
.map(|_| {
let source = Arc::clone(&source);
tokio::spawn(async move {
AssertUnwindSafe(source.request(SegmentId::from(0)))
.catch_unwind()
.await
})
})
.collect();
let joined =
tokio::time::timeout(std::time::Duration::from_secs(5), future::join_all(readers))
.await
.expect("a reader hung instead of observing the driver panic");
let mut original_panics = 0;
for reader in joined {
match reader.expect("reader task panicked at the join boundary") {
Err(payload) => {
assert!(
panic_message(&*payload).contains("read-at panic"),
"got: {:?}",
panic_message(&*payload)
);
original_panics += 1;
}
Ok(result) => assert!(result.is_err(), "expected a dropped-by-runtime error"),
}
}
assert_eq!(
original_panics, 1,
"exactly one reader should re-raise the original driver panic"
);
}
#[derive(Clone)]
struct SlowErrReadAt;
impl VortexReadAt for SlowErrReadAt {
fn concurrency(&self) -> usize {
4
}
fn size(&self) -> BoxFuture<'static, VortexResult<u64>> {
async { Ok(1024) }.boxed()
}
fn read_at(
&self,
offset: u64,
_length: usize,
_alignment: Alignment,
) -> BoxFuture<'static, VortexResult<BufferHandle>> {
async move {
for _ in 0..(offset as usize % 5 + 1) {
tokio::task::yield_now().await;
}
vortex_bail!("slow read done")
}
.boxed()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn read_driver_concurrent_reads_make_progress() {
let n = 16u32;
let segments: Arc<[SegmentSpec]> = (0..n)
.map(|i| SegmentSpec {
offset: u64::from(i) * 4,
length: 4,
alignment: Alignment::none(),
})
.collect();
let metrics = DefaultMetricsRegistry::default();
let source = Arc::new(FileSegmentSource::open(
segments,
SlowErrReadAt,
TokioRuntime::current(),
RequestMetrics::new(&metrics, vec![]),
));
let tasks: Vec<_> = (0..n)
.map(|i| {
let source = Arc::clone(&source);
tokio::spawn(async move { source.request(SegmentId::from(i)).await })
})
.collect();
let joined =
tokio::time::timeout(std::time::Duration::from_secs(5), future::join_all(tasks)).await;
assert!(joined.is_ok(), "concurrent reads stalled before completing");
}
}