Skip to content

Commit 1ca3a34

Browse files
committed
claude CR stuff
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 54e5096 commit 1ca3a34

4 files changed

Lines changed: 131 additions & 129 deletions

File tree

vortex-layout/src/scan/layout.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ use vortex_scan::selection::Selection;
4040
use vortex_session::VortexSession;
4141

4242
use crate::LayoutReaderRef;
43+
use crate::scan::limit::LimitedStream;
44+
use crate::scan::limit::RowBudget;
4345
use crate::scan::limit::SharedRowLimit;
44-
use crate::scan::limit::limit_array_stream_shared;
4546
use crate::scan::scan_builder::ScanBuilder;
4647

4748
/// An implementation of a [`DataSource`] that reads data from a [`LayoutReaderRef`].
@@ -333,7 +334,11 @@ impl Partition for LayoutReaderSplit {
333334
let dtype = builder.dtype()?;
334335
// Use into_stream() which creates a LazyScanStream that spawns individual I/O
335336
// tasks onto the runtime, enabling parallel execution across executor threads.
336-
let stream = limit_array_stream_shared(builder.into_stream()?, shared_limit);
337+
let stream = builder.into_stream()?.boxed();
338+
let stream = match shared_limit {
339+
Some(limit) => LimitedStream::new(stream, RowBudget::Shared(limit)).boxed(),
340+
None => stream,
341+
};
337342

338343
Ok(ArrayStreamExt::boxed(ArrayStreamAdapter::new(
339344
dtype, stream,

vortex-layout/src/scan/limit.rs

Lines changed: 56 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,15 @@ use futures::StreamExt;
1313
use futures::stream;
1414
use futures::stream::BoxStream;
1515
use vortex_array::ArrayRef;
16+
use vortex_error::VortexExpect;
1617
use vortex_error::VortexResult;
1718

18-
pub(crate) fn limit_array_stream<S>(
19-
stream: S,
20-
limit: Option<u64>,
21-
) -> BoxStream<'static, VortexResult<ArrayRef>>
22-
where
23-
S: Stream<Item = VortexResult<ArrayRef>> + Send + 'static,
24-
{
25-
match limit {
26-
Some(limit) => RowLimitedStream::new(stream.boxed(), limit).boxed(),
27-
None => stream.boxed(),
28-
}
29-
}
30-
3119
/// A row limit shared by streams that execute independent scan partitions.
20+
///
21+
/// The shared budget gives "at most `limit` rows in total" semantics without any ordering
22+
/// guarantee across the streams that share it: whichever stream produces a chunk first claims
23+
/// the budget first. It is therefore only correct for consumers that treat the combined output
24+
/// as unordered (e.g. a bare `LIMIT n`), not for order-preserving cross-partition consumption.
3225
#[derive(Clone)]
3326
pub(crate) struct SharedRowLimit(Arc<AtomicU64>);
3427

@@ -52,99 +45,87 @@ impl SharedRowLimit {
5245
}
5346
}
5447
}
55-
}
5648

57-
pub(crate) fn limit_array_stream_shared<S>(
58-
stream: S,
59-
limit: Option<SharedRowLimit>,
60-
) -> BoxStream<'static, VortexResult<ArrayRef>>
61-
where
62-
S: Stream<Item = VortexResult<ArrayRef>> + Send + 'static,
63-
{
64-
match limit {
65-
Some(limit) => SharedRowLimitedStream::new(stream.boxed(), limit).boxed(),
66-
None => stream.boxed(),
49+
fn is_exhausted(&self) -> bool {
50+
self.0.load(Ordering::Relaxed) == 0
6751
}
6852
}
6953

70-
struct RowLimitedStream {
71-
inner: BoxStream<'static, VortexResult<ArrayRef>>,
72-
remaining: u64,
73-
}
74-
75-
impl RowLimitedStream {
76-
fn new(inner: BoxStream<'static, VortexResult<ArrayRef>>, remaining: u64) -> Self {
77-
Self { inner, remaining }
78-
}
79-
80-
fn abort_pending(&mut self) {
81-
let inner = std::mem::replace(&mut self.inner, stream::empty().boxed());
82-
drop(inner);
83-
}
54+
/// The remaining rows a [`LimitedStream`] may emit, either privately or shared across streams.
55+
pub(crate) enum RowBudget {
56+
/// A budget private to a single stream.
57+
Local(u64),
58+
/// A budget shared across streams executing independent scan partitions.
59+
Shared(SharedRowLimit),
8460
}
8561

86-
impl Stream for RowLimitedStream {
87-
type Item = VortexResult<ArrayRef>;
88-
89-
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
90-
if self.remaining == 0 {
91-
return Poll::Ready(None);
62+
impl RowBudget {
63+
/// Reserve up to `requested` rows from the budget.
64+
///
65+
/// Returns `(reserved, exhausted)` where `reserved <= requested` and `exhausted` is true
66+
/// once the budget has reached zero.
67+
fn reserve(&mut self, requested: u64) -> (u64, bool) {
68+
match self {
69+
RowBudget::Local(remaining) => {
70+
let reserved = (*remaining).min(requested);
71+
*remaining -= reserved;
72+
(reserved, *remaining == 0)
73+
}
74+
RowBudget::Shared(shared) => shared.reserve(requested),
9275
}
76+
}
9377

94-
match self.inner.as_mut().poll_next(cx) {
95-
Poll::Ready(Some(Ok(chunk))) => {
96-
let chunk_len = chunk.len() as u64;
97-
if chunk_len <= self.remaining {
98-
self.remaining -= chunk_len;
99-
if self.remaining == 0 {
100-
self.abort_pending();
101-
}
102-
Poll::Ready(Some(Ok(chunk)))
103-
} else {
104-
let limit = match usize::try_from(self.remaining) {
105-
Ok(limit) => limit,
106-
Err(_) => unreachable!("remaining rows cannot exceed the current chunk"),
107-
};
108-
self.remaining = 0;
109-
self.abort_pending();
110-
Poll::Ready(Some(chunk.slice(0..limit)))
111-
}
112-
}
113-
other => other,
78+
fn is_exhausted(&self) -> bool {
79+
match self {
80+
RowBudget::Local(remaining) => *remaining == 0,
81+
RowBudget::Shared(shared) => shared.is_exhausted(),
11482
}
11583
}
11684
}
11785

118-
struct SharedRowLimitedStream {
86+
/// Wraps a stream, emitting chunks until its [`RowBudget`] is exhausted, then terminating.
87+
pub(crate) struct LimitedStream {
11988
inner: BoxStream<'static, VortexResult<ArrayRef>>,
120-
limit: SharedRowLimit,
89+
budget: RowBudget,
12190
}
12291

123-
impl SharedRowLimitedStream {
124-
fn new(inner: BoxStream<'static, VortexResult<ArrayRef>>, limit: SharedRowLimit) -> Self {
125-
Self { inner, limit }
92+
impl LimitedStream {
93+
pub(crate) fn new(
94+
inner: BoxStream<'static, VortexResult<ArrayRef>>,
95+
budget: RowBudget,
96+
) -> Self {
97+
Self { inner, budget }
12698
}
12799

100+
/// Drop the inner stream so no further work (including spawned split tasks) is polled.
128101
fn abort_pending(&mut self) {
129-
let inner = std::mem::replace(&mut self.inner, stream::empty().boxed());
130-
drop(inner);
102+
self.inner = stream::empty().boxed();
131103
}
132104
}
133105

134-
impl Stream for SharedRowLimitedStream {
106+
impl Stream for LimitedStream {
135107
type Item = VortexResult<ArrayRef>;
136108

137109
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
110+
// Avoid reading a chunk we have no budget for. For a shared budget this also stops a
111+
// partition whose siblings already consumed the limit.
112+
if self.budget.is_exhausted() {
113+
self.abort_pending();
114+
return Poll::Ready(None);
115+
}
116+
138117
match self.inner.as_mut().poll_next(cx) {
139118
Poll::Ready(Some(Ok(chunk))) => {
140119
let chunk_len = chunk.len() as u64;
141-
let (reserved, exhausted) = self.limit.reserve(chunk_len);
120+
let (reserved, exhausted) = self.budget.reserve(chunk_len);
142121

143122
if exhausted {
144123
self.abort_pending();
145124
}
146125

147126
if reserved == 0 {
127+
// Either the budget was already exhausted (stop), or this is an empty chunk
128+
// while the budget still has room (pass it through).
148129
if exhausted {
149130
Poll::Ready(None)
150131
} else {
@@ -153,11 +134,8 @@ impl Stream for SharedRowLimitedStream {
153134
} else if reserved == chunk_len {
154135
Poll::Ready(Some(Ok(chunk)))
155136
} else {
156-
let limit = match usize::try_from(reserved) {
157-
Ok(limit) => limit,
158-
Err(_) => unreachable!("reserved rows cannot exceed the current chunk"),
159-
};
160-
self.abort_pending();
137+
let limit = usize::try_from(reserved)
138+
.vortex_expect("reserved rows are bounded by the chunk length");
161139
Poll::Ready(Some(chunk.slice(0..limit)))
162140
}
163141
}

vortex-layout/src/scan/multi.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ use vortex_session::VortexSession;
5757
use vortex_utils::parallelism::get_available_parallelism;
5858

5959
use crate::LayoutReaderRef;
60+
use crate::scan::limit::LimitedStream;
61+
use crate::scan::limit::RowBudget;
6062
use crate::scan::limit::SharedRowLimit;
61-
use crate::scan::limit::limit_array_stream_shared;
6263
use crate::scan::scan_builder::ScanBuilder;
6364

6465
/// Default concurrency for opening deferred readers.
@@ -540,7 +541,11 @@ impl Partition for MultiLayoutPartition {
540541
}
541542

542543
let dtype = builder.dtype()?;
543-
let stream = limit_array_stream_shared(builder.into_stream()?, shared_limit);
544+
let stream = builder.into_stream()?.boxed();
545+
let stream = match shared_limit {
546+
Some(limit) => LimitedStream::new(stream, RowBudget::Shared(limit)).boxed(),
547+
None => stream,
548+
};
544549

545550
Ok(ArrayStreamExt::boxed(ArrayStreamAdapter::new(
546551
dtype, stream,

vortex-layout/src/scan/repeated_scan.rs

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::sync::Arc;
99
use futures::FutureExt;
1010
use futures::Stream;
1111
use futures::StreamExt;
12+
use futures::stream::BoxStream;
1213
use itertools::Either;
1314
use itertools::Itertools;
1415
use vortex_array::ArrayRef;
@@ -21,14 +22,16 @@ use vortex_array::stream::ArrayStreamAdapter;
2122
use vortex_error::VortexExpect;
2223
use vortex_error::VortexResult;
2324
use vortex_io::runtime::BlockingRuntime;
25+
use vortex_io::runtime::Task;
2426
use vortex_io::session::RuntimeSessionExt;
2527
use vortex_scan::selection::Selection;
2628
use vortex_session::VortexSession;
2729
use vortex_utils::parallelism::get_available_parallelism;
2830

2931
use crate::LayoutReaderRef;
3032
use crate::scan::filter::FilterExpr;
31-
use crate::scan::limit::limit_array_stream;
33+
use crate::scan::limit::LimitedStream;
34+
use crate::scan::limit::RowBudget;
3235
use crate::scan::splits::Splits;
3336
use crate::scan::tasks::TaskContext;
3437
use crate::scan::tasks::TaskFuture;
@@ -172,15 +175,19 @@ impl RepeatedScan {
172175
}
173176
}
174177

178+
fn task_context(&self) -> Arc<TaskContext> {
179+
Arc::new(TaskContext {
180+
filter: self.filter.clone().map(|f| Arc::new(FilterExpr::new(f))),
181+
reader: Arc::clone(&self.layout_reader),
182+
projection: self.projection.clone(),
183+
})
184+
}
185+
175186
pub(crate) fn execute(
176187
&self,
177188
row_range: Option<Range<u64>>,
178189
) -> VortexResult<Vec<TaskFuture<Option<ArrayRef>>>> {
179-
let ctx = Arc::new(TaskContext {
180-
filter: self.filter.clone().map(|f| Arc::new(FilterExpr::new(f))),
181-
reader: Arc::clone(&self.layout_reader),
182-
projection: self.projection.clone(),
183-
});
190+
let ctx = self.task_context();
184191

185192
let mut limit = self.limit;
186193
let mut tasks = Vec::new();
@@ -209,57 +216,64 @@ impl RepeatedScan {
209216
row_range: Option<Range<u64>>,
210217
) -> VortexResult<impl Stream<Item = VortexResult<ArrayRef>> + Send + 'static> {
211218
let num_workers = get_available_parallelism().unwrap_or(1);
219+
let concurrency = self.concurrency * num_workers;
212220
let handle = self.session.handle();
213221

222+
// With both a filter and a limit we cannot know each split's output row count ahead of
223+
// time, so split tasks are built lazily as the stream is polled. `buffered`'s read-ahead
224+
// (bounded by `concurrency`) registers IO for splits eagerly, but only as far as the
225+
// limit requires: `limit_array_stream` drops the inner stream once the limit is reached,
226+
// capping over-read at `concurrency` splits.
214227
if self.filter.is_some() && self.limit.is_some() {
215-
let ctx = Arc::new(TaskContext {
216-
filter: self.filter.clone().map(|f| Arc::new(FilterExpr::new(f))),
217-
reader: Arc::clone(&self.layout_reader),
218-
projection: self.projection.clone(),
219-
});
228+
let ctx = self.task_context();
220229
let selection = self.selection.clone();
221-
let ordered = self.ordered;
222-
let limit = self.limit;
223-
let stream = futures::stream::iter(self.split_ranges(row_range)).map(move |range| {
224-
let ctx = Arc::clone(&ctx);
225-
let handle = handle.clone();
226-
let selection = selection.clone();
227-
async move {
230+
let tasks =
231+
futures::stream::iter(self.split_ranges(row_range)).filter_map(move |range| {
232+
// Build the row mask and split task synchronously so the IO system sees the
233+
// split's ranges as soon as `buffered` pulls it, without cloning `selection`.
228234
let row_mask = selection.row_mask(&range);
229-
if row_mask.mask().all_false() {
230-
return Ok(None);
231-
}
235+
let spawned = (!row_mask.mask().all_false()).then(|| {
236+
let task = split_exec(Arc::clone(&ctx), row_mask, None)
237+
.unwrap_or_else(|err| async move { Err(err) }.boxed());
238+
handle.spawn(task)
239+
});
240+
async move { spawned }
241+
});
232242

233-
let task = split_exec(ctx, row_mask, None)?;
234-
handle.spawn(task).await
235-
}
236-
.boxed()
237-
});
238-
let stream = if ordered {
239-
stream.buffered(1).boxed()
240-
} else {
241-
stream.buffer_unordered(1).boxed()
242-
};
243-
244-
return Ok(limit_array_stream(
245-
stream.filter_map(|chunk| async move { chunk.transpose() }),
246-
limit,
247-
));
243+
return Ok(schedule(tasks, self.ordered, concurrency, self.limit));
248244
}
249245

250-
let stream =
246+
// No filter (or no limit): build every task eagerly so the IO system sees all split
247+
// ranges up front. A no-filter limit is applied exactly per split inside `execute`.
248+
let tasks =
251249
futures::stream::iter(self.execute(row_range)?).map(move |task| handle.spawn(task));
252-
let concurrency = self.concurrency * num_workers;
253-
let stream = if self.ordered {
254-
stream.buffered(concurrency).boxed()
255-
} else {
256-
stream.buffer_unordered(concurrency).boxed()
257-
};
258250

259-
Ok(limit_array_stream(
260-
stream.filter_map(|chunk| async move { chunk.transpose() }),
261-
self.limit,
262-
))
251+
Ok(schedule(tasks, self.ordered, concurrency, self.limit))
252+
}
253+
}
254+
255+
/// Spawn-buffer a stream of split tasks, transposing empty splits away and applying `limit`.
256+
fn schedule<S>(
257+
tasks: S,
258+
ordered: bool,
259+
concurrency: usize,
260+
limit: Option<u64>,
261+
) -> BoxStream<'static, VortexResult<ArrayRef>>
262+
where
263+
S: Stream<Item = Task<VortexResult<Option<ArrayRef>>>> + Send + 'static,
264+
{
265+
let stream = if ordered {
266+
tasks.buffered(concurrency).boxed()
267+
} else {
268+
tasks.buffer_unordered(concurrency).boxed()
269+
};
270+
let stream = stream
271+
.filter_map(|chunk| async move { chunk.transpose() })
272+
.boxed();
273+
274+
match limit {
275+
Some(limit) => LimitedStream::new(stream, RowBudget::Local(limit)).boxed(),
276+
None => stream,
263277
}
264278
}
265279

0 commit comments

Comments
 (0)