use std::ops::Range;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use arrow_array::{RecordBatch, RecordBatchReader};
use arrow_schema::{Schema, SchemaRef};
use async_trait::async_trait;
use futures::task::{ArcWake, AtomicWaker, waker_ref};
use futures::{Stream, StreamExt as _, TryStreamExt as _, stream};
use parking_lot::Mutex;
use tokio::sync::mpsc;
use tokio::task::{JoinHandle, spawn_blocking};
use uuid::Uuid;
use crate::scandal::TableBinding;
pub use crate::scandal::api::{ColumnStatistics, ScanStatistics};
use crate::scandal::expression::Expression;
use crate::scandal::types::require_exact_filter;
pub use crate::scandal::types::{
FilterClause, FilterSupport, ScanMetricsSnapshot, ScanOptions, ScanRowRange, StorageCounters,
StorageScope, TableSplit, WindowCounters,
};
use crate::{Error, ResourceAddress, SortColumn, TableIdentity, WriteLayout};
type MetricsProvider = Arc<dyn Fn() -> Result<Option<ScanMetricsSnapshot>, Error> + Send + Sync>;
#[derive(Clone, Default)]
pub struct ScanMetrics {
provider: Option<Arc<Mutex<Option<MetricsProvider>>>>,
}
impl ScanMetrics {
pub(crate) fn from_snapshot_fn(
provider: impl Fn() -> Result<ScanMetricsSnapshot, Error> + Send + Sync + 'static,
) -> Self {
Self {
provider: Some(Arc::new(Mutex::new(Some(Arc::new(move || {
provider().map(Some)
}))))),
}
}
pub fn snapshot(&self) -> Result<Option<ScanMetricsSnapshot>, Error> {
let provider = self
.provider
.as_ref()
.and_then(|provider| provider.lock().clone());
provider.map_or(Ok(None), |provider| provider())
}
fn pending() -> Self {
Self {
provider: Some(Arc::new(Mutex::new(None))),
}
}
fn bind(&self, source: Self) {
if let (Some(provider), Some(_)) = (&self.provider, &source.provider) {
*provider.lock() = Some(Arc::new(move || source.snapshot()));
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ScanSplit {
pub fragment: u64,
pub rows: Range<u64>,
pub estimated_rows: Option<u64>,
pub estimated_bytes: Option<u64>,
pub fragment_id: Option<Uuid>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkItem {
pub split: usize,
pub rows: Range<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Take {
pub split: usize,
pub positions: Vec<u64>,
}
#[derive(Clone, Debug)]
pub(crate) struct ScanInfo {
pub schema: Schema,
pub filter_clauses: Vec<FilterClause>,
pub write_layout: Option<WriteLayout>,
pub statistics: Option<ScanStatistics>,
}
pub trait SplitFilter: Send + Sync {
fn current(&self) -> Option<Expression>;
}
#[async_trait]
pub(crate) trait ScanSource: Send + Sync {
async fn read(&self, narrowing: Option<Arc<dyn SplitFilter>>) -> Result<BatchStream, Error>;
fn ordered_partitions(&self) -> Option<(usize, Vec<SortColumn>)> {
None
}
fn read_partitions(
&self,
_narrowing: Option<Arc<dyn SplitFilter>>,
_lanes: usize,
_batch_rows: usize,
) -> Option<Vec<BatchStream>> {
None
}
async fn table_binding(&self) -> Result<Option<TableBinding>, Error> {
Ok(None)
}
fn estimated_rows(&self) -> Option<u64> {
None
}
fn splits(&self) -> &[ScanSplit] {
&[]
}
async fn take(&self, _takes: Vec<Take>) -> Result<BatchStream, Error> {
Err(Error::invalid("this scan has no splits to take rows from"))
}
async fn read_work(&self, _work: Vec<WorkItem>) -> Result<WorkStream, Error> {
Err(Error::invalid("this scan has no splits to read"))
}
async fn count_rows(&self, _work: &[WorkItem]) -> Result<Vec<u64>, Error> {
Err(Error::invalid("this scan has no splits to count"))
}
}
#[derive(Clone)]
pub struct Scan {
pub(crate) address: Option<ResourceAddress>,
identity: TableIdentity,
options: ScanOptions,
schema: SchemaRef,
filter_clauses: Vec<FilterClause>,
write_layout: Option<WriteLayout>,
statistics: Option<ScanStatistics>,
stream_count: usize,
ordering: Vec<SortColumn>,
source: Arc<dyn ScanSource>,
}
impl Scan {
pub(crate) fn new(
identity: TableIdentity,
options: ScanOptions,
info: ScanInfo,
source: Arc<dyn ScanSource>,
) -> Result<Self, Error> {
require_exact_filter(&info.filter_clauses)?;
let max_streams = options.stream_count()?;
let ordered = source.ordered_partitions().filter(|(count, _)| {
(*count == 1
|| (options.partitioned && options.limit.is_none() && options.row_range.is_none()))
&& options.emit.is_none()
});
let (stream_count, ordering) = ordered
.map(|(count, ordering)| (count.min(max_streams), ordering))
.unwrap_or((max_streams, Vec::new()));
Ok(Self {
address: None,
identity,
options,
schema: Arc::new(info.schema),
filter_clauses: info.filter_clauses,
write_layout: info.write_layout,
statistics: info.statistics,
stream_count,
ordering,
source,
})
}
pub(crate) async fn table_binding(&self) -> Result<Option<TableBinding>, Error> {
self.source.table_binding().await
}
pub fn identity(&self) -> TableIdentity {
self.identity
}
pub fn resolved_address(&self) -> Option<&ResourceAddress> {
self.address.as_ref()
}
pub fn options(&self) -> &ScanOptions {
&self.options
}
pub fn schema(&self) -> SchemaRef {
Arc::clone(&self.schema)
}
pub fn write_layout(&self) -> Option<&WriteLayout> {
self.write_layout.as_ref()
}
pub fn statistics(&self) -> Option<&ScanStatistics> {
self.statistics.as_ref()
}
pub fn ordering(&self) -> &[SortColumn] {
&self.ordering
}
pub fn filter_clauses(&self) -> &[FilterClause] {
&self.filter_clauses
}
pub fn estimated_rows(&self) -> Option<u64> {
self.source.estimated_rows()
}
pub fn stream_count(&self) -> usize {
self.stream_count
}
pub fn splits(&self) -> &[ScanSplit] {
self.source.splits()
}
pub async fn read_work(&self, work: Vec<WorkItem>) -> Result<WorkStream, Error> {
self.validate_work(&work)?;
self.source.read_work(work).await
}
pub async fn count_rows(&self, work: &[WorkItem]) -> Result<Vec<u64>, Error> {
self.validate_work(work)?;
self.source.count_rows(work).await
}
pub async fn take(&self, takes: Vec<Take>) -> Result<BatchStream, Error> {
if self.options.filter.is_some()
|| self.options.limit.is_some()
|| self.options.emit.is_some()
{
return Err(Error::invalid(
"scans with a filter, a limit or an emit take no rows by position",
));
}
let splits = self.splits().len();
if let Some(take) = takes.iter().find(|take| take.split >= splits) {
return Err(Error::invalid(format!(
"a take names split {} of a scan with {splits} splits",
take.split
)));
}
self.source.take(takes).await
}
fn validate_work(&self, work: &[WorkItem]) -> Result<(), Error> {
if self.options.limit.is_some() || self.options.emit.is_some() {
return Err(Error::invalid(
"scans with a limit or an emit read no work items",
));
}
let splits = self.splits();
for item in work {
let split = splits.get(item.split).ok_or_else(|| {
Error::invalid(format!(
"work names split {} of a scan with {} splits",
item.split,
splits.len()
))
})?;
if item.rows.start > item.rows.end
|| item.rows.start < split.rows.start
|| item.rows.end > split.rows.end
{
return Err(Error::invalid(format!(
"work rows {:?} are outside split {} rows {:?}",
item.rows, item.split, split.rows
)));
}
}
Ok(())
}
pub fn into_stream(self) -> BatchStream {
if !self.ordering.is_empty() {
return self
.source
.read_partitions(None, 1, self.options.batch_rows.unwrap_or(65_536))
.expect("ordered source supplies fixed partitions")
.pop()
.expect("one ordered lane");
}
self.open_stream(None)
}
pub fn into_streams(self) -> Vec<ScanStream> {
self.lanes(None)
}
pub fn into_narrowed_streams(self, filter: Arc<dyn SplitFilter>) -> Vec<ScanStream> {
self.lanes(Some(filter))
}
fn lanes(self, narrowing: Option<Arc<dyn SplitFilter>>) -> Vec<ScanStream> {
if !self.ordering.is_empty() {
return self
.source
.read_partitions(
narrowing,
self.stream_count,
self.options.batch_rows.unwrap_or(65_536),
)
.expect("ordered source supplies fixed partitions")
.into_iter()
.map(|stream| ScanStream {
schema: stream.schema(),
metrics: stream.metrics(),
stream: Box::pin(stream),
})
.collect();
}
let count = self.stream_count;
let schema = self.schema();
let stream = self.open_stream(narrowing);
let metrics = stream.metrics();
let shared = Arc::new(LaneSource {
state: Mutex::new(LaneState {
stream: Some(stream),
active: 0,
failure: None,
}),
waiters: (0..count).map(|_| AtomicWaker::new()).collect(),
});
(0..count)
.map(|index| ScanStream {
schema: Arc::clone(&schema),
metrics: metrics.clone(),
stream: Box::pin(Lane {
shared: Arc::clone(&shared),
index,
active: false,
}),
})
.collect()
}
fn open_stream(self, narrowing: Option<Arc<dyn SplitFilter>>) -> BatchStream {
let schema = self.schema();
let metrics = if self.options.collect_metrics {
ScanMetrics::pending()
} else {
ScanMetrics::default()
};
let execution_metrics = metrics.clone();
let opened = stream::once(async move {
let batches = self.source.read(narrowing).await?;
execution_metrics.bind(batches.metrics());
if batches.schema() != self.schema {
return Err(Error::arrow(
"opened stream schema differs from scan schema",
));
}
Ok(batches)
});
BatchStream::from_stream(schema, opened.try_flatten()).with_metrics(metrics)
}
}
pub struct ScanStream {
schema: SchemaRef,
metrics: ScanMetrics,
stream: Pin<Box<dyn Stream<Item = Result<RecordBatch, Error>> + Send>>,
}
impl ScanStream {
pub fn metrics(&self) -> ScanMetrics {
self.metrics.clone()
}
pub fn schema(&self) -> SchemaRef {
Arc::clone(&self.schema)
}
pub fn into_stream(self) -> BatchStream {
BatchStream::from_stream(self.schema, self.stream).with_metrics(self.metrics)
}
}
struct LaneSource {
state: Mutex<LaneState>,
waiters: Vec<AtomicWaker>,
}
struct LaneState {
stream: Option<BatchStream>,
active: usize,
failure: Option<String>,
}
impl ArcWake for LaneSource {
fn wake_by_ref(shared: &Arc<Self>) {
for waiter in &shared.waiters {
waiter.wake();
}
}
}
struct Lane {
shared: Arc<LaneSource>,
index: usize,
active: bool,
}
impl Stream for Lane {
type Item = Result<RecordBatch, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if !self.active {
self.shared.state.lock().active += 1;
self.active = true;
}
self.shared.waiters[self.index].register(cx.waker());
let waker = waker_ref(&self.shared);
let mut source_context = Context::from_waker(&waker);
let result = {
let mut state = self.shared.state.lock();
if let Some(failure) = &state.failure {
return Poll::Ready(Some(Err(Error::arrow(failure))));
}
let Some(stream) = &mut state.stream else {
return Poll::Ready(None);
};
let result = Pin::new(stream).poll_next(&mut source_context);
match &result {
Poll::Ready(Some(Err(error))) => {
state.failure = Some(error.to_string());
state.stream = None;
}
Poll::Ready(None) => state.stream = None,
_ => {}
}
result
};
if result.is_ready() {
LaneSource::wake_by_ref(&self.shared);
}
result
}
}
impl Drop for Lane {
fn drop(&mut self) {
self.shared.waiters[self.index].take();
if self.active {
let mut state = self.shared.state.lock();
state.active -= 1;
if state.active == 0 && state.stream.take().is_some() {
state.failure = Some("scan execution cancelled".into());
}
}
}
}
#[derive(Clone, Debug)]
pub struct WorkBatch {
pub batch: RecordBatch,
pub item: usize,
pub next_row: u64,
}
pub struct WorkStream {
schema: SchemaRef,
stream: Pin<Box<dyn Stream<Item = Result<WorkBatch, Error>> + Send>>,
}
impl WorkStream {
pub(crate) fn new(
schema: SchemaRef,
stream: impl Stream<Item = Result<WorkBatch, Error>> + Send + 'static,
) -> Self {
Self {
schema,
stream: Box::pin(stream),
}
}
pub fn schema(&self) -> SchemaRef {
Arc::clone(&self.schema)
}
pub async fn next_batch(&mut self) -> Option<Result<WorkBatch, Error>> {
self.next().await
}
}
impl Stream for WorkStream {
type Item = Result<WorkBatch, Error>;
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.stream.as_mut().poll_next(context)
}
}
pub struct BatchStream {
schema: SchemaRef,
metrics: ScanMetrics,
stream: Pin<Box<dyn Stream<Item = Result<RecordBatch, Error>> + Send>>,
}
impl BatchStream {
pub fn from_stream(
schema: SchemaRef,
stream: impl Stream<Item = Result<RecordBatch, Error>> + Send + 'static,
) -> Self {
let expected = Arc::clone(&schema);
let stream = stream::unfold(Some(stream.boxed()), move |source| {
let expected = Arc::clone(&expected);
async move {
let mut source = source?;
let batch = source.next().await?.and_then(|batch| {
if batch.schema() != expected {
return Err(Error::arrow("batch schema differs from scan schema"));
}
Ok(batch)
});
let source = batch.is_ok().then_some(source);
Some((batch, source))
}
});
Self {
schema,
metrics: ScanMetrics::default(),
stream: Box::pin(stream),
}
}
pub fn with_metrics(mut self, metrics: ScanMetrics) -> Self {
self.metrics = metrics;
self
}
pub fn metrics(&self) -> ScanMetrics {
self.metrics.clone()
}
#[expect(
clippy::disallowed_methods,
reason = "The open SDK adapts blocking Arrow callbacks without a Vortex runtime"
)]
pub fn from_reader(mut reader: Box<dyn RecordBatchReader + Send>) -> Self {
let schema = reader.schema();
let (sender, receiver) = mpsc::channel(2);
let worker = spawn_blocking(move || {
while !sender.is_closed() {
let Some(batch) = reader.next() else { break };
let failed = batch.is_err();
if sender.blocking_send(batch.map_err(Error::arrow)).is_err() || failed {
break;
}
}
});
Self::from_stream(schema, ReaderStream { receiver, worker })
}
pub fn schema(&self) -> SchemaRef {
Arc::clone(&self.schema)
}
pub async fn next_batch(&mut self) -> Option<Result<RecordBatch, Error>> {
self.next().await
}
}
impl Stream for BatchStream {
type Item = Result<RecordBatch, Error>;
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.stream.as_mut().poll_next(context)
}
}
struct ReaderStream {
receiver: mpsc::Receiver<Result<RecordBatch, Error>>,
worker: JoinHandle<()>,
}
impl Stream for ReaderStream {
type Item = Result<RecordBatch, Error>;
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.receiver.poll_recv(context)
}
}
impl Drop for ReaderStream {
fn drop(&mut self) {
self.receiver.close();
self.worker.abort();
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use arrow_array::Int64Array;
use arrow_schema::{DataType, Field};
use uuid::Uuid;
use super::*;
struct WorkSource {
reads: AtomicUsize,
drops: Arc<AtomicUsize>,
schema: SchemaRef,
fail: bool,
}
struct WorkLifetime(Arc<AtomicUsize>);
impl Drop for WorkLifetime {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
#[async_trait]
impl ScanSource for WorkSource {
async fn read(&self, _: Option<Arc<dyn SplitFilter>>) -> Result<BatchStream, Error> {
self.reads.fetch_add(1, Ordering::SeqCst);
let schema = Arc::clone(&self.schema);
let fail = self.fail;
let guard = WorkLifetime(Arc::clone(&self.drops));
let rows = Arc::new(AtomicUsize::new(0));
let counters = Arc::clone(&rows);
let metrics = ScanMetrics::from_snapshot_fn(move || {
Ok(ScanMetricsSnapshot {
output_rows: (counters.load(Ordering::Relaxed) as u64).into(),
..Default::default()
})
});
Ok(BatchStream::from_stream(
Arc::clone(&schema),
stream::unfold((0, guard), move |(index, guard)| {
let schema = Arc::clone(&schema);
let rows = Arc::clone(&rows);
async move {
if index == 100 {
return None;
}
let batch = if fail && index == 1 {
Err(Error::arrow("work failed"))
} else {
Ok(RecordBatch::try_new(
schema,
vec![Arc::new(Int64Array::from(vec![index]))],
)
.unwrap())
};
if batch.is_ok() {
rows.fetch_add(1, Ordering::Relaxed);
}
Some((batch, (index + 1, guard)))
}
}),
)
.with_metrics(metrics))
}
}
fn work_scan(fail: bool) -> (Scan, Arc<WorkSource>) {
let schema = Arc::new(Schema::new(vec![Field::new(
"value",
DataType::Int64,
false,
)]));
let source = Arc::new(WorkSource {
reads: AtomicUsize::new(0),
drops: Arc::new(AtomicUsize::new(0)),
schema: Arc::clone(&schema),
fail,
});
let scan = Scan::new(
TableIdentity {
resource_id: Uuid::new_v4(),
version_id: Uuid::new_v4(),
},
ScanOptions {
collect_metrics: true,
partitioned: true,
max_streams: Some(2),
..Default::default()
},
ScanInfo {
statistics: None,
write_layout: None,
schema: schema.as_ref().clone(),
filter_clauses: vec![],
},
source.clone(),
)
.unwrap();
(scan, source)
}
#[tokio::test]
async fn metrics_follow_execution_lifetime_and_replay() {
let (scan, _) = work_scan(false);
let mut lanes = scan.clone().into_streams().into_iter();
let first = lanes.next().unwrap();
let second = lanes.next().unwrap();
let first_metrics = first.metrics();
let second_metrics = second.metrics();
assert!(first_metrics.snapshot().unwrap().is_none());
let mut first = first.into_stream();
let mut second = second.into_stream();
first.next_batch().await.unwrap().unwrap();
second.next_batch().await.unwrap().unwrap();
assert_eq!(first_metrics.snapshot().unwrap().unwrap().output_rows.0, 2);
assert_eq!(
first_metrics.snapshot().unwrap(),
second_metrics.snapshot().unwrap()
);
drop(first);
drop(second);
assert_eq!(first_metrics.snapshot().unwrap().unwrap().output_rows.0, 2);
let mut replay = scan.into_stream();
let replay_metrics = replay.metrics();
assert!(replay_metrics.snapshot().unwrap().is_none());
replay.next_batch().await.unwrap().unwrap();
assert_eq!(replay_metrics.snapshot().unwrap().unwrap().output_rows.0, 1);
assert_eq!(first_metrics.snapshot().unwrap().unwrap().output_rows.0, 2);
}
#[tokio::test]
async fn lanes_share_work_and_replay_creates_a_fresh_execution() {
let (scan, source) = work_scan(false);
let mut lanes = scan
.clone()
.into_streams()
.into_iter()
.map(ScanStream::into_stream);
let mut slow = lanes.next().unwrap();
let mut fast = lanes.next().unwrap();
assert_eq!(source.reads.load(Ordering::SeqCst), 0);
let first = slow.next_batch().await.unwrap().unwrap();
assert_eq!(
first
.column(0)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.value(0),
0
);
for value in 1..100 {
let batch = fast.next_batch().await.unwrap().unwrap();
assert_eq!(
batch
.column(0)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.value(0),
value
);
}
assert!(fast.next_batch().await.is_none());
assert!(slow.next_batch().await.is_none());
assert_eq!(source.reads.load(Ordering::SeqCst), 1);
assert_eq!(source.drops.load(Ordering::SeqCst), 1);
let batches = scan.into_stream().collect::<Vec<_>>().await;
assert_eq!(batches.len(), 100);
assert_eq!(source.reads.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn lane_failure_and_last_active_lane_cancellation_release_work() {
let (scan, source) = work_scan(true);
let mut lanes = scan.into_streams().into_iter().map(ScanStream::into_stream);
let mut first = lanes.next().unwrap();
let mut second = lanes.next().unwrap();
first.next_batch().await.unwrap().unwrap();
assert!(second.next_batch().await.unwrap().is_err());
assert!(first.next_batch().await.unwrap().is_err());
assert!(first.next_batch().await.is_none());
assert!(second.next_batch().await.is_none());
assert_eq!(source.drops.load(Ordering::SeqCst), 1);
let (scan, source) = work_scan(false);
let mut lanes = scan.into_streams().into_iter().map(ScanStream::into_stream);
let mut active = lanes.next().unwrap();
let mut idle = lanes.next().unwrap();
active.next_batch().await.unwrap().unwrap();
drop(active);
assert_eq!(
source.drops.load(Ordering::SeqCst),
1,
"idle lanes must not retain active I/O"
);
assert!(idle.next_batch().await.unwrap().is_err());
assert_eq!(source.reads.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn batch_schema_mismatch_is_terminal() {
let expected = Arc::new(Schema::new(vec![Field::new(
"expected",
DataType::Int64,
false,
)]));
let wrong = RecordBatch::try_new(
Arc::new(Schema::new(vec![Field::new(
"wrong",
DataType::Int64,
false,
)])),
vec![Arc::new(Int64Array::from(vec![1]))],
)
.unwrap();
let valid = RecordBatch::try_new(
Arc::clone(&expected),
vec![Arc::new(Int64Array::from(vec![2]))],
)
.unwrap();
let polled = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&polled);
let source = stream::iter([Ok(wrong), Ok(valid)]).inspect(move |_| {
counter.fetch_add(1, Ordering::SeqCst);
});
let mut batches = BatchStream::from_stream(expected, source);
assert!(matches!(
batches.next_batch().await,
Some(Err(Error::Arrow(_)))
));
assert!(batches.next_batch().await.is_none());
assert_eq!(polled.load(Ordering::SeqCst), 1);
}
}