@@ -13,22 +13,15 @@ use futures::StreamExt;
1313use futures:: stream;
1414use futures:: stream:: BoxStream ;
1515use vortex_array:: ArrayRef ;
16+ use vortex_error:: VortexExpect ;
1617use 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 ) ]
3326pub ( 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 }
0 commit comments