Skip to content

Commit 073f9ac

Browse files
fix: Surface ingestion errors (parseablehq#1652)
- DiskWriter and MemWriter expect and unwrap replaced - New cli env var `P_DATAFUSION_TARGET_PARTITIONS` for controlling number of partitions (default num cpu / 2) - Streaming response uses unbounded channel now --------- Co-authored-by: Nikhil Sinha <nikhil.sinha@parseable.com>
1 parent b67b828 commit 073f9ac

6 files changed

Lines changed: 98 additions & 42 deletions

File tree

src/cli.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818

1919
use clap::{Parser, value_parser};
20-
use std::{env, fs, path::PathBuf};
20+
use std::{env, fs, ops::Div, path::PathBuf};
2121

2222
use url::Url;
2323

@@ -194,6 +194,16 @@ pub struct Options {
194194
)]
195195
pub max_connections: usize,
196196

197+
// DataFusion target partitions
198+
#[arg(
199+
long,
200+
env = "P_DATAFUSION_TARGET_PARTITIONS",
201+
default_value_t = num_cpus::get().div(2).max(1) as u64,
202+
value_parser = value_parser!(u64).range(1..),
203+
help = "Number of partitions for DF to split execution into"
204+
)]
205+
pub target_partitions: u64,
206+
197207
#[arg(
198208
long = "origin",
199209
env = "P_ORIGIN_URI",

src/handlers/airplane.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ impl FlightService for AirServiceImpl {
235235

236236
if event.is_some() {
237237
// Clear staging of stream once airplane has taxied
238-
PARSEABLE.get_or_create_stream(&stream_name, &None).clear();
238+
let _ = PARSEABLE.get_or_create_stream(&stream_name, &None).clear();
239239
}
240240

241241
let time = time.elapsed().as_secs_f64();

src/parseable/staging/writer.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,17 @@ impl<const N: usize> Default for MemWriter<N> {
135135
}
136136

137137
impl<const N: usize> MemWriter<N> {
138-
pub fn push(&mut self, schema_key: &str, rb: &RecordBatch) {
138+
pub fn push(&mut self, schema_key: &str, rb: &RecordBatch) -> Result<(), StagingError> {
139139
if !self.schema_map.contains(schema_key) {
140140
self.schema_map.insert(schema_key.to_owned());
141-
self.schema = Schema::try_merge([self.schema.clone(), (*rb.schema()).clone()]).unwrap();
141+
self.schema = Schema::try_merge([self.schema.clone(), (*rb.schema()).clone()])?;
142142
}
143143

144144
if let Some(record) = self.mutable_buffer.push(rb) {
145-
let record = concat_records(&Arc::new(self.schema.clone()), &record);
145+
let record = concat_records(&Arc::new(self.schema.clone()), &record)?;
146146
self.read_buffer.push(record);
147147
}
148+
Ok(())
148149
}
149150

150151
pub fn clear(&mut self) {
@@ -154,23 +155,29 @@ impl<const N: usize> MemWriter<N> {
154155
self.mutable_buffer.inner.clear();
155156
}
156157

157-
pub fn recordbatch_cloned(&self, schema: &Arc<Schema>) -> Vec<RecordBatch> {
158+
pub fn recordbatch_cloned(
159+
&self,
160+
schema: &Arc<Schema>,
161+
) -> Result<Vec<RecordBatch>, StagingError> {
158162
let mut read_buffer = self.read_buffer.clone();
159163
if !self.mutable_buffer.inner.is_empty() {
160-
let rb = concat_records(schema, &self.mutable_buffer.inner);
164+
let rb = concat_records(schema, &self.mutable_buffer.inner)?;
161165
read_buffer.push(rb)
162166
}
163167

164-
read_buffer
168+
Ok(read_buffer
165169
.into_iter()
166170
.map(|rb| adapt_batch(schema, &rb))
167-
.collect()
171+
.collect())
168172
}
169173
}
170174

171-
fn concat_records(schema: &Arc<Schema>, record: &[RecordBatch]) -> RecordBatch {
175+
fn concat_records(
176+
schema: &Arc<Schema>,
177+
record: &[RecordBatch],
178+
) -> Result<RecordBatch, StagingError> {
172179
let records = record.iter().map(|x| adapt_batch(schema, x)).collect_vec();
173-
concat_batches(schema, records.iter()).unwrap()
180+
Ok(concat_batches(schema, records.iter())?)
174181
}
175182

176183
#[derive(Debug, Default)]

src/parseable/streams.rs

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,15 @@ impl Stream {
186186
OBJECT_STORE_DATA_GRANULARITY,
187187
);
188188
let file_path = self.data_path.join(&filename);
189-
let mut writer = DiskWriter::try_new(file_path, &record.schema(), range)
190-
.expect("File and RecordBatch both are checked");
189+
let mut writer = DiskWriter::try_new(file_path, &record.schema(), range)?;
191190

192191
writer.write(record)?;
193192
guard.disk.insert(filename, writer);
194193
}
195194
};
196195
}
197196

198-
guard.mem.push(schema_key, record);
197+
guard.mem.push(schema_key, record)?;
199198

200199
Ok(())
201200
}
@@ -532,26 +531,46 @@ impl Stream {
532531
Ok(())
533532
}
534533

535-
pub fn recordbatches_cloned(&self, schema: &Arc<Schema>) -> Vec<RecordBatch> {
536-
self.writer.lock().unwrap().mem.recordbatch_cloned(schema)
537-
}
538-
539-
pub fn clear(&self) {
540-
self.writer.lock().unwrap().mem.clear();
534+
pub fn recordbatches_cloned(
535+
&self,
536+
schema: &Arc<Schema>,
537+
) -> Result<Vec<RecordBatch>, StagingError> {
538+
let writer = self.writer.lock().map_err(|poisoned| {
539+
StagingError::PoisonError(PoisonError::new(format!(
540+
"Writer lock poisoned while cloning record batches for stream {} - {}",
541+
self.stream_name, poisoned
542+
)))
543+
})?;
544+
545+
writer.mem.recordbatch_cloned(schema)
546+
}
547+
548+
pub fn clear(&self) -> Result<(), StagingError> {
549+
self.writer
550+
.lock()
551+
.map_err(|poisoned| {
552+
StagingError::PoisonError(PoisonError::new(format!(
553+
"Writer lock poisoned while clearing stream {} - {}",
554+
self.stream_name, poisoned
555+
)))
556+
})?
557+
.mem
558+
.clear();
559+
Ok(())
541560
}
542561

543-
pub fn flush(&self, forced: bool) {
562+
pub fn flush(&self, forced: bool) -> Result<(), StagingError> {
544563
let _span = info_span!("flush", stream_name = %self.stream_name, forced).entered();
545564
// Swap out stale writers under the lock, drop them after releasing it.
546565
// DiskWriter::Drop does I/O (IPC finish + file rename) so dropping
547566
// outside the lock avoids blocking concurrent push() calls.
548567
let stale_writers = {
549-
let mut writer = self.writer.lock().unwrap_or_else(|_| {
550-
panic!(
551-
"Writer lock poisoned while flushing data for stream {}",
552-
self.stream_name
553-
)
554-
});
568+
let mut writer = self.writer.lock().map_err(|poisoned| {
569+
StagingError::PoisonError(PoisonError::new(format!(
570+
"Writer lock poisoned while flushing data for stream {} - {}",
571+
self.stream_name, poisoned
572+
)))
573+
})?;
555574
writer.mem.clear();
556575

557576
let mut old_disk = HashMap::new();
@@ -567,6 +586,7 @@ impl Stream {
567586
};
568587
// DiskWriter::Drop I/O happens here, outside the lock
569588
drop(stale_writers);
589+
Ok(())
570590
}
571591

572592
fn parquet_writer_props(
@@ -1306,7 +1326,7 @@ impl Stream {
13061326
// Force flush for init or shutdown signals to convert all .part files to .arrows
13071327
// For regular cycles, use false to only flush non-current writers
13081328
let forced = init_signal || shutdown_signal;
1309-
self.flush(forced);
1329+
self.flush(forced)?;
13101330
info!(
13111331
"Flushing stream ({}) took: {}s",
13121332
self.stream_name,
@@ -1717,7 +1737,7 @@ mod tests {
17171737
StreamType::UserDefined,
17181738
)
17191739
.unwrap();
1720-
staging.flush(true);
1740+
staging.flush(true).unwrap();
17211741
}
17221742

17231743
#[test]

src/query/mod.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use datafusion::sql::parser::DFParser;
4444
use datafusion::sql::resolve::resolve_table_references;
4545
use datafusion::sql::sqlparser::dialect::PostgreSqlDialect;
4646
use futures::Stream;
47+
use futures::StreamExt;
4748
use itertools::Itertools;
4849
use once_cell::sync::{Lazy, OnceCell};
4950
use serde::{Deserialize, Serialize};
@@ -55,7 +56,7 @@ use std::sync::{Arc, RwLock};
5556
use std::task::{Context, Poll};
5657
use sysinfo::System;
5758
use tokio::runtime::Runtime;
58-
use tokio_stream::wrappers::ReceiverStream;
59+
use tokio_stream::wrappers::UnboundedReceiverStream;
5960
use tracing::Instrument;
6061

6162
use self::error::ExecuteError;
@@ -251,7 +252,8 @@ impl Query {
251252
.with_prefer_existing_sort(true)
252253
//batch size has been made configurable via environment variable
253254
//default value is 20000
254-
.with_batch_size(PARSEABLE.options.execution_batch_size);
255+
.with_batch_size(PARSEABLE.options.execution_batch_size)
256+
.with_target_partitions(PARSEABLE.options.target_partitions as usize);
255257

256258
// Pushdown filters allows DF to push the filters as far down in the plan as possible
257259
// and thus, reducing the number of rows decoded
@@ -260,10 +262,22 @@ impl Query {
260262
// Reorder filters allows DF to decide the order of filters minimizing the cost of filter evaluation
261263
config.options_mut().execution.parquet.reorder_filters = true;
262264
config.options_mut().execution.parquet.binary_as_string = true;
265+
// Bump footer-read hint from the 512 KiB default. Streams with
266+
// many label columns + page-indexed value columns can have
267+
// parquet footers in the 1-2 MiB range; sizing the hint above
268+
// the actual footer collapses the two-read fallback into a
269+
// single GET per file, saving a round trip on every file open.
270+
config.options_mut().execution.parquet.metadata_size_hint = Some(2 * 1024 * 1024);
263271
config
264272
.options_mut()
265273
.execution
266274
.use_row_number_estimates_to_optimize_partitioning = true;
275+
config.options_mut().execution.parquet.enable_page_index = true;
276+
config
277+
.options_mut()
278+
.execution
279+
.parquet
280+
.max_predicate_cache_size = Some(1024 * 1024 * 1024);
267281

268282
//adding this config as it improves query performance as explained here -
269283
// https://github.com/apache/datafusion/pull/13101
@@ -348,23 +362,21 @@ impl Query {
348362
});
349363

350364
let partition_streams = execute_stream_partitioned(plan.clone(), task_ctx.clone())?;
351-
let n = partition_streams.len();
352-
// Bound channel so a slow consumer backpressures producers — caps peak memory.
353-
let (tx, rx) = tokio::sync::mpsc::channel::<
365+
366+
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<
354367
Result<RecordBatch, datafusion::error::DataFusionError>,
355-
>((num_cpus::get() * 4).max(n * 2).max(1));
368+
>();
356369

357-
for s in partition_streams {
370+
for s in partition_streams.into_iter() {
358371
let wrapped =
359372
PartitionedMetricMonitor::new(s, monitor_state.clone(), tenant_id.clone());
360373
let tx = tx.clone();
361374
let span = tracing::Span::current();
362375
tokio::spawn(
363376
async move {
364377
let mut stream: SendableRecordBatchStream = Box::pin(wrapped);
365-
use futures::StreamExt;
366378
while let Some(batch) = stream.next().await {
367-
if tx.send(batch).await.is_err() {
379+
if tx.send(batch).is_err() {
368380
break;
369381
}
370382
}
@@ -374,7 +386,7 @@ impl Query {
374386
}
375387
drop(tx);
376388

377-
let merged = ReceiverStream::new(rx);
389+
let merged = UnboundedReceiverStream::new(rx);
378390
let final_stream = RecordBatchStreamAdapter::new(plan.schema(), merged);
379391
Either::Right(Box::pin(final_stream) as SendableRecordBatchStream)
380392
};

src/query/stream_schema_provider.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,19 @@ impl StandardTableProvider {
153153

154154
// parquet file source, default table parquet options
155155
let file_source = if let Some(phyiscal_expr) = filters {
156-
ParquetSource::new(self.schema.clone()).with_predicate(phyiscal_expr)
156+
ParquetSource::new(self.schema.clone())
157+
.with_predicate(phyiscal_expr)
158+
.with_pushdown_filters(true)
159+
.with_reorder_filters(true)
157160
} else {
158161
ParquetSource::new(self.schema.clone())
162+
.with_pushdown_filters(true)
163+
.with_reorder_filters(true)
159164
};
160165

161166
let mut conf_builder = FileScanConfigBuilder::new(object_store_url, file_source.into())
162167
.with_statistics(statistics)
163-
.with_batch_size(Some(20000))
168+
.with_batch_size(Some(PARSEABLE.options.execution_batch_size))
164169
.with_constraints(Constraints::default())
165170
.with_file_groups(file_groups)
166171
.with_output_ordering(vec![LexOrdering::new([sort_expr]).unwrap()]);
@@ -263,7 +268,9 @@ impl StandardTableProvider {
263268
};
264269

265270
// Staging arrow exection plan
266-
let records = staging.recordbatches_cloned(&self.schema);
271+
let records = staging
272+
.recordbatches_cloned(&self.schema)
273+
.map_err(|e| DataFusionError::Internal(format!("Error during staging exec- {e}")))?;
267274
let arrow_exec = reversed_mem_table(records, self.schema.clone())?
268275
.scan(state, projection, filters, limit)
269276
.await?;

0 commit comments

Comments
 (0)