blob: 491562c573236d5462cc1eb5708f250ca259db3a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::{
read_ipc_compressed, CometPartitioning, CompressionCodec, ShuffleWriterDestination,
ShuffleWriterExec,
};
use arrow::array::{Array, Int32Array, RecordBatch, RecordBatchOptions};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use arrow::row::{RowConverter, SortField};
use datafusion::common::{DataFusionError, Result};
use datafusion::datasource::memory::MemorySourceConfig;
use datafusion::datasource::source::DataSourceExec;
use datafusion::execution::config::SessionConfig;
use datafusion::execution::runtime_env::RuntimeEnvBuilder;
use datafusion::physical_expr::expressions::Column;
use datafusion::physical_expr::{LexOrdering, PhysicalSortExpr};
use datafusion::physical_plan::common::collect;
use datafusion::physical_plan::{ChildrenPropertiesMode, ExecutionPlan, ReplaceChildrenOptions};
use datafusion::prelude::SessionContext;
use datafusion_comet_jni_bridge::ShufflePartitionPusher;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::{Arc, Mutex};
type RecordedFrame = (i32, Vec<u8>);
#[derive(Default)]
struct RecordingPusher {
frames: Mutex<Vec<RecordedFrame>>,
}
impl RecordingPusher {
fn frames(&self) -> Vec<RecordedFrame> {
self.frames.lock().unwrap().clone()
}
}
impl ShufflePartitionPusher for RecordingPusher {
fn push_partition_data(&self, partition_id: i32, data: &[u8]) -> Result<()> {
self.frames
.lock()
.unwrap()
.push((partition_id, data.to_vec()));
Ok(())
}
}
#[derive(Debug)]
struct CallbackSentinel;
impl Display for CallbackSentinel {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str("original typed shuffle callback failure")
}
}
impl Error for CallbackSentinel {}
struct ExternalFailingPusher;
impl ShufflePartitionPusher for ExternalFailingPusher {
fn push_partition_data(&self, _partition_id: i32, _data: &[u8]) -> Result<()> {
Err(DataFusionError::External(Box::new(CallbackSentinel)))
}
}
fn int_batch(start: i32, count: i32) -> RecordBatch {
let schema = Arc::new(Schema::new(vec![Field::new(
"value",
DataType::Int32,
false,
)]));
let values = Int32Array::from_iter_values(start..start + count);
RecordBatch::try_new(schema, vec![Arc::new(values)]).unwrap()
}
fn memory_input(batches: Vec<RecordBatch>, schema: SchemaRef) -> Arc<dyn ExecutionPlan> {
let config = MemorySourceConfig::try_new(std::slice::from_ref(&batches), schema, None).unwrap();
Arc::new(DataSourceExec::new(Arc::new(config)))
}
fn rss_execution(
batches: Vec<RecordBatch>,
schema: SchemaRef,
partitioning: CometPartitioning,
pusher: Arc<dyn ShufflePartitionPusher>,
codec: CompressionCodec,
max_frame_size: usize,
max_buffer_bytes: Option<usize>,
) -> ShuffleWriterExec {
ShuffleWriterExec::try_new_with_destination(
memory_input(batches, schema),
partitioning,
codec,
ShuffleWriterDestination::Rss {
pusher,
max_frame_size,
},
false,
1024 * 1024,
max_buffer_bytes,
)
.unwrap()
}
fn run_execution(plan: &dyn ExecutionPlan) -> Result<Vec<RecordBatch>> {
let runtime = Arc::new(
RuntimeEnvBuilder::new()
.with_memory_limit(1024 * 1024 * 1024, 1.0)
.build()
.unwrap(),
);
let context = SessionContext::new_with_config_rt(SessionConfig::new(), runtime);
let stream = plan.execute(0, context.task_ctx())?;
tokio::runtime::Runtime::new()
.unwrap()
.block_on(collect(stream))
}
fn decode_frame(frame: &[u8]) -> RecordBatch {
assert!(frame.len() >= 16, "shuffle frame must contain its header");
let encoded_length = u64::from_le_bytes(frame[..8].try_into().unwrap());
assert_eq!(
usize::try_from(encoded_length).unwrap() + 8,
frame.len(),
"each callback must receive one complete length-prefixed frame"
);
read_ipc_compressed(&frame[16..]).unwrap()
}
fn values_in_frames(frames: &[RecordedFrame]) -> Vec<i32> {
frames
.iter()
.flat_map(|(_, frame)| {
let batch = decode_frame(frame);
batch
.column(0)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.values()
.to_vec()
})
.collect()
}
fn metric_value(execution: &ShuffleWriterExec, name: &str) -> usize {
execution
.metrics()
.unwrap()
.iter()
.find(|metric| metric.value().name() == name)
.map(|metric| metric.value().as_usize())
.unwrap_or_default()
}
fn assert_original_callback_failure(error: DataFusionError) {
match error {
DataFusionError::External(original) => assert!(
original.downcast_ref::<CallbackSentinel>().is_some(),
"the original typed callback error must remain downcastable"
),
other => panic!("callback failure was wrapped or replaced: {other}"),
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn rss_single_partition_preserves_complete_frames_and_codecs() {
let first = int_batch(0, 16);
let second = int_batch(16, 16);
for codec in [
CompressionCodec::None,
CompressionCodec::Lz4Frame,
CompressionCodec::Snappy,
CompressionCodec::Zstd(1),
] {
let pusher = Arc::new(RecordingPusher::default());
let execution = rss_execution(
vec![first.clone(), second.clone()],
first.schema(),
CometPartitioning::SinglePartition,
pusher.clone(),
codec,
1024 * 1024,
None,
);
assert!(run_execution(&execution).unwrap().is_empty());
let frames = pusher.frames();
assert_eq!(frames.len(), 2);
assert!(frames.iter().all(|(partition_id, _)| *partition_id == 0));
assert_eq!(values_in_frames(&frames), (0..32).collect::<Vec<_>>());
assert_eq!(metric_value(&execution, "input_batches"), 2);
assert_eq!(metric_value(&execution, "output_rows"), 32);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn rss_multi_partition_supports_hash_range_and_round_robin() {
let batch = int_batch(0, 100);
let expression = Arc::new(Column::new("value", 0));
let ordering =
LexOrdering::new(vec![PhysicalSortExpr::new_default(expression.clone())]).unwrap();
let converter = RowConverter::new(vec![SortField::new(DataType::Int32)]).unwrap();
let boundary_values: Arc<dyn Array> = Arc::new(Int32Array::from(vec![25, 50, 75]));
let boundaries = converter
.convert_columns(&[boundary_values])
.unwrap()
.iter()
.map(|row| row.owned())
.collect();
for partitioning in [
CometPartitioning::Hash(vec![expression], 4),
CometPartitioning::RangePartitioning(ordering, 4, Arc::new(converter), boundaries),
CometPartitioning::RoundRobin(4, 0),
] {
let pusher = Arc::new(RecordingPusher::default());
let execution = rss_execution(
vec![batch.clone()],
batch.schema(),
partitioning,
pusher.clone(),
CompressionCodec::Lz4Frame,
1024 * 1024,
None,
);
run_execution(&execution).unwrap();
let frames = pusher.frames();
assert!(!frames.is_empty());
assert!(frames
.iter()
.all(|(partition_id, _)| (0..4).contains(partition_id)));
let mut values = values_in_frames(&frames);
values.sort_unstable();
assert_eq!(values, (0..100).collect::<Vec<_>>());
assert_eq!(metric_value(&execution, "output_rows"), 100);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn rss_empty_schema_preserves_row_counts_in_partition_zero() {
let schema = Arc::new(Schema::empty());
let batch = RecordBatch::try_new_with_options(
schema.clone(),
vec![],
&RecordBatchOptions::new().with_row_count(Some(37)),
)
.unwrap();
let pusher = Arc::new(RecordingPusher::default());
let execution = rss_execution(
vec![batch.clone(), batch],
schema,
CometPartitioning::RoundRobin(4, 0),
pusher.clone(),
CompressionCodec::None,
1024 * 1024,
None,
);
run_execution(&execution).unwrap();
let frames = pusher.frames();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].0, 0);
assert_eq!(decode_frame(&frames[0].1).num_rows(), 74);
assert_eq!(metric_value(&execution, "output_rows"), 74);
}
#[test]
fn rss_empty_schema_without_rows_does_not_push_frames() {
let schema = Arc::new(Schema::empty());
let batch = RecordBatch::try_new_with_options(
schema.clone(),
vec![],
&RecordBatchOptions::new().with_row_count(Some(0)),
)
.unwrap();
let pusher = Arc::new(RecordingPusher::default());
let execution = rss_execution(
vec![batch],
schema,
CometPartitioning::RoundRobin(4, 0),
pusher.clone(),
CompressionCodec::None,
1024 * 1024,
None,
);
run_execution(&execution).unwrap();
assert!(pusher.frames().is_empty());
}
#[test]
#[cfg_attr(miri, ignore)]
fn rss_spill_pushes_remotely_without_reporting_disk_bytes() {
let batches = (0..6)
.map(|index| int_batch(index * 512, 512))
.collect::<Vec<_>>();
let schema = batches[0].schema();
let pusher = Arc::new(RecordingPusher::default());
let execution = rss_execution(
batches,
schema,
CometPartitioning::Hash(vec![Arc::new(Column::new("value", 0))], 4),
pusher.clone(),
CompressionCodec::Lz4Frame,
1024 * 1024,
Some(256),
);
run_execution(&execution).unwrap();
assert!(metric_value(&execution, "spill_count") > 0);
assert!(metric_value(&execution, "memory_spilled_bytes") > 0);
assert_eq!(metric_value(&execution, "spilled_bytes"), 0);
let mut values = values_in_frames(&pusher.frames());
values.sort_unstable();
assert_eq!(values, (0..3072).collect::<Vec<_>>());
}
#[test]
fn rss_preserves_typed_callback_failures_while_inserting() {
let batch = int_batch(0, 8);
let execution = rss_execution(
vec![batch.clone()],
batch.schema(),
CometPartitioning::SinglePartition,
Arc::new(ExternalFailingPusher),
CompressionCodec::None,
1024 * 1024,
None,
);
assert_original_callback_failure(run_execution(&execution).unwrap_err());
}
#[test]
fn rss_preserves_typed_callback_failures_while_finalizing_and_spilling() {
for max_buffer_bytes in [None, Some(1)] {
let batch = int_batch(0, 64);
let execution = rss_execution(
vec![batch.clone()],
batch.schema(),
CometPartitioning::Hash(vec![Arc::new(Column::new("value", 0))], 4),
Arc::new(ExternalFailingPusher),
CompressionCodec::None,
1024 * 1024,
max_buffer_bytes,
);
assert_original_callback_failure(run_execution(&execution).unwrap_err());
}
}
#[test]
fn rss_rejects_invalid_and_oversized_frames_without_pushing() {
for max_frame_size in [0, 1] {
let batch = int_batch(0, 8);
let pusher = Arc::new(RecordingPusher::default());
let execution = rss_execution(
vec![batch.clone()],
batch.schema(),
CometPartitioning::SinglePartition,
pusher.clone(),
CompressionCodec::None,
max_frame_size,
None,
);
let error = run_execution(&execution).unwrap_err();
assert!(error.to_string().contains("frame"));
assert!(pusher.frames().is_empty());
}
}
#[test]
fn rss_callback_survives_execution_plan_child_replacement() {
let original = int_batch(0, 4);
let replacement = int_batch(10, 6);
let pusher = Arc::new(RecordingPusher::default());
let execution = rss_execution(
vec![original.clone()],
original.schema(),
CometPartitioning::SinglePartition,
pusher.clone(),
CompressionCodec::None,
1024 * 1024,
None,
);
let replacement_input = memory_input(vec![replacement.clone()], replacement.schema());
let rewritten = Arc::new(execution)
.replace_children(
vec![replacement_input],
ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
)
.unwrap();
run_execution(rewritten.as_ref()).unwrap();
assert_eq!(
values_in_frames(&pusher.frames()),
(10..16).collect::<Vec<_>>()
);
}
#[test]
fn explicit_local_destination_preserves_data_and_index_files() {
let batch = int_batch(0, 8);
let directory = tempfile::tempdir().unwrap();
let data_file = directory.path().join("shuffle.data");
let index_file = directory.path().join("shuffle.index");
let execution = ShuffleWriterExec::try_new_with_destination(
memory_input(vec![batch.clone()], batch.schema()),
CometPartitioning::SinglePartition,
CompressionCodec::None,
ShuffleWriterDestination::Local {
output_data_file: data_file.to_str().unwrap().to_string(),
output_index_file: index_file.to_str().unwrap().to_string(),
},
false,
1024 * 1024,
None,
)
.unwrap();
run_execution(&execution).unwrap();
let frame = std::fs::read(data_file).unwrap();
assert_eq!(decode_frame(&frame).num_rows(), 8);
assert_eq!(std::fs::read(index_file).unwrap().len(), 16);
}