1use std::fmt;
9use std::sync::Arc;
10
11use arrow::datatypes::SchemaRef;
12
13use crate::physical::budget::{ResourceReservation, ResourceScope};
14use crate::physical::{
15 record_batch_memory_size, BatchOpenContext, BatchSource, BatchSourceFactory,
16 ColumnarSegmentCursor, ColumnarSegmentProvider, SourceLimit, StreamBatch,
17};
18use crate::{DataFrameError, Result};
19
20#[derive(Clone)]
22pub struct ColumnarSegmentBatchSourceFactory {
23 provider: Arc<dyn ColumnarSegmentProvider>,
24}
25
26impl fmt::Debug for ColumnarSegmentBatchSourceFactory {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 f.debug_struct("ColumnarSegmentBatchSourceFactory")
29 .field("source_name", &self.provider.source_name())
30 .finish_non_exhaustive()
31 }
32}
33
34impl ColumnarSegmentBatchSourceFactory {
35 pub fn new(provider: Arc<dyn ColumnarSegmentProvider>) -> Self {
37 Self { provider }
38 }
39}
40
41impl BatchSourceFactory for ColumnarSegmentBatchSourceFactory {
42 fn source_name(&self) -> &'static str {
43 self.provider.source_name()
44 }
45
46 fn schema(&self) -> Result<SchemaRef> {
47 Err(DataFrameError::streaming_unsupported(
48 "columnar_segment_scan",
49 "schema_is_available_after_bounded_open",
50 ))
51 }
52
53 fn source_limits(&self) -> Vec<SourceLimit> {
54 self.provider.source_limits()
55 }
56
57 fn open(&self, context: BatchOpenContext) -> Result<Box<dyn BatchSource>> {
58 let mut metadata_reservation = context
62 .budget
63 .reserve(ResourceScope::Source, context.options.memory_limit_bytes)?;
64 let cursor = self.provider.open(context.options.memory_limit_bytes)?;
65 let retained_metadata = cursor.metadata_footprint_upper_bound();
66 if retained_metadata > context.options.memory_limit_bytes {
67 return Err(DataFrameError::resource_limit_exceeded(
68 context.options.memory_limit_bytes,
69 retained_metadata,
70 context.budget.max_in_flight_batches(),
71 context.budget.usage().reserved_batches,
72 ResourceScope::Source,
73 ));
74 }
75 metadata_reservation.shrink_to(retained_metadata);
76 let schema = cursor.schema();
77
78 Ok(Box::new(ColumnarSegmentBatchSource {
79 context,
80 cursor,
81 schema,
82 metadata_reservation: Some(metadata_reservation),
83 }))
84 }
85}
86
87struct ColumnarSegmentBatchSource {
88 context: BatchOpenContext,
89 cursor: Box<dyn ColumnarSegmentCursor>,
90 schema: SchemaRef,
91 metadata_reservation: Option<ResourceReservation>,
92}
93
94impl BatchSource for ColumnarSegmentBatchSource {
95 fn schema(&self) -> SchemaRef {
96 self.schema.clone()
97 }
98
99 fn next_batch(&mut self) -> Result<Option<StreamBatch>> {
100 let Some(plan) = self.cursor.next_batch_plan()? else {
101 return Ok(None);
102 };
103 let reservation_upper_bound = plan.reservation_upper_bound();
104 let reservation = self
105 .context
106 .budget
107 .reserve_batch(ResourceScope::Decode, reservation_upper_bound)?;
108 let Some(batch) = self.cursor.next_batch()? else {
109 drop(reservation);
110 return Err(DataFrameError::schema_mismatch(
111 "V08 cursor returned no batch after declaring a batch plan",
112 ));
113 };
114
115 let actual_arrow_bytes = record_batch_memory_size(&batch);
116 if actual_arrow_bytes > plan.arrow_allocation_upper_bound {
117 drop(reservation);
118 return Err(DataFrameError::resource_limit_exceeded(
119 self.context.budget.memory_limit_bytes(),
120 self.context
121 .budget
122 .usage()
123 .reserved_bytes
124 .saturating_add(actual_arrow_bytes),
125 self.context.budget.max_in_flight_batches(),
126 self.context.budget.usage().reserved_batches,
127 ResourceScope::Decode,
128 ));
129 }
130
131 Ok(Some(StreamBatch::new(batch, reservation)))
132 }
133
134 fn close(&mut self) -> Result<()> {
135 let result = self.cursor.close();
136 self.metadata_reservation.take();
137 result
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use std::collections::VecDeque;
144 use std::num::NonZeroUsize;
145 use std::sync::atomic::{AtomicUsize, Ordering};
146
147 use arrow::array::{ArrayRef, Int64Array};
148 use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
149 use arrow::record_batch::RecordBatch;
150
151 use super::*;
152 use crate::physical::{
153 ColumnarSegmentBatchPlan, ColumnarSegmentCursor, ColumnarSegmentProvider, DataFrameStream,
154 PlanSubject, SourceLimit,
155 };
156 use crate::{DataFrameError, Result};
157
158 #[derive(Clone)]
159 struct TestProvider {
160 batches: Vec<RecordBatch>,
161 plan: ColumnarSegmentBatchPlan,
162 metadata_bytes: u64,
163 next_calls: Arc<AtomicUsize>,
164 close_calls: Arc<AtomicUsize>,
165 }
166
167 impl ColumnarSegmentProvider for TestProvider {
168 fn source_limits(&self) -> Vec<SourceLimit> {
169 vec![SourceLimit {
170 source: PlanSubject::ColumnarSegmentScan,
171 code: "requires_v08_chunked_layout",
172 description: "legacy V2 segment blobs are not streaming sources",
173 }]
174 }
175
176 fn open(&self, _metadata_limit_bytes: u64) -> Result<Box<dyn ColumnarSegmentCursor>> {
177 Ok(Box::new(TestCursor {
178 batches: self.batches.clone().into(),
179 plan: self.plan,
180 metadata_bytes: self.metadata_bytes,
181 next_calls: self.next_calls.clone(),
182 close_calls: self.close_calls.clone(),
183 }))
184 }
185 }
186
187 struct TestCursor {
188 batches: VecDeque<RecordBatch>,
189 plan: ColumnarSegmentBatchPlan,
190 metadata_bytes: u64,
191 next_calls: Arc<AtomicUsize>,
192 close_calls: Arc<AtomicUsize>,
193 }
194
195 impl ColumnarSegmentCursor for TestCursor {
196 fn schema(&self) -> SchemaRef {
197 schema()
198 }
199
200 fn metadata_footprint_upper_bound(&self) -> u64 {
201 self.metadata_bytes
202 }
203
204 fn next_batch_plan(&self) -> Result<Option<ColumnarSegmentBatchPlan>> {
205 Ok((!self.batches.is_empty()).then_some(self.plan))
206 }
207
208 fn next_batch(&mut self) -> Result<Option<RecordBatch>> {
209 self.next_calls.fetch_add(1, Ordering::SeqCst);
210 Ok(self.batches.pop_front())
211 }
212
213 fn close(&mut self) -> Result<()> {
214 self.close_calls.fetch_add(1, Ordering::SeqCst);
215 Ok(())
216 }
217 }
218
219 fn schema() -> SchemaRef {
220 Arc::new(Schema::new(vec![Field::new(
221 "value",
222 DataType::Int64,
223 false,
224 )]))
225 }
226
227 fn batch(value: i64) -> RecordBatch {
228 RecordBatch::try_new(
229 schema(),
230 vec![Arc::new(Int64Array::from(vec![value])) as ArrayRef],
231 )
232 .unwrap()
233 }
234
235 fn options(limit: u64) -> crate::physical::budget::StreamOptions {
236 crate::physical::budget::StreamOptions::new(
237 limit,
238 NonZeroUsize::new(1).unwrap(),
239 NonZeroUsize::new(1).unwrap(),
240 )
241 }
242
243 #[test]
244 fn source_reserves_before_cursor_read_and_releases_on_exhaustion() {
245 let next_calls = Arc::new(AtomicUsize::new(0));
246 let close_calls = Arc::new(AtomicUsize::new(0));
247 let provider = TestProvider {
248 batches: vec![batch(1), batch(2)],
249 plan: ColumnarSegmentBatchPlan {
250 decoded_bytes: 128,
251 arrow_allocation_upper_bound: 1024,
252 },
253 metadata_bytes: 256,
254 next_calls: next_calls.clone(),
255 close_calls: close_calls.clone(),
256 };
257 let factory = ColumnarSegmentBatchSourceFactory::new(Arc::new(provider));
258 let mut stream = DataFrameStream::from_factory(&factory, options(4096)).unwrap();
259
260 assert_eq!(next_calls.load(Ordering::SeqCst), 0);
261 assert_eq!(stream.next_batch().unwrap().unwrap().height(), 1);
262 assert_eq!(stream.next_batch().unwrap().unwrap().height(), 1);
263 assert!(stream.next_batch().unwrap().is_none());
264 assert_eq!(close_calls.load(Ordering::SeqCst), 1);
265 assert_eq!(stream.budget().usage().reserved_bytes, 0);
266 }
267
268 #[test]
269 fn oversized_row_group_is_rejected_before_cursor_read() {
270 let next_calls = Arc::new(AtomicUsize::new(0));
271 let close_calls = Arc::new(AtomicUsize::new(0));
272 let provider = TestProvider {
273 batches: vec![batch(1)],
274 plan: ColumnarSegmentBatchPlan {
275 decoded_bytes: 2048,
276 arrow_allocation_upper_bound: 2048,
277 },
278 metadata_bytes: 128,
279 next_calls: next_calls.clone(),
280 close_calls: close_calls.clone(),
281 };
282 let factory = ColumnarSegmentBatchSourceFactory::new(Arc::new(provider));
283 let mut stream = DataFrameStream::from_factory(&factory, options(1024)).unwrap();
284
285 assert!(matches!(
286 stream.next_batch(),
287 Err(DataFrameError::StreamFailed {
288 code: "resource_limit_exceeded",
289 ..
290 })
291 ));
292 assert_eq!(next_calls.load(Ordering::SeqCst), 0);
293 assert_eq!(close_calls.load(Ordering::SeqCst), 1);
294 }
295}