blob: 33816caed2e86df142bec57e0d03153e5b6e3431 [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 std::io::{BufRead, BufReader, Read, Take, Write};
use arrow::{array::ArrayRef, datatypes::SchemaRef};
use auron_jni_bridge::{
conf,
conf::{IntConf, StringConf},
is_jni_bridge_inited,
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use datafusion::common::Result;
use once_cell::sync::OnceCell;
use zstd::stream::{raw, raw::Operation, zio};
use crate::{
df_execution_err,
io::{read_one_batch, write_one_batch},
};
/// Block-level compressor used by [`IpcCompressionWriter`]. Unlike
/// [`IoCompressionWriter`] (which is used for whole-stream compression, e.g.
/// spilling), this type is specifically designed so that the underlying
/// codec context (lz4's compression hash table / zstd's `CCtx`) can be
/// reused across consecutive blocks instead of being recreated from scratch
/// every time a block is finished.
enum BlockCompressor {
LZ4(lz4_flex::frame::FrameEncoder<VecBufferWrite>),
// wrapped in Option so finish_current_buf() can take the inner zio::Writer
// out, reinit the underlying raw::Encoder (CCtx), and put a freshly
// constructed zio::Writer (pointing at the next block's buffer) back in.
ZSTD(Option<zio::Writer<VecBufferWrite, raw::Encoder<'static>>>),
}
impl BlockCompressor {
fn new(shared_buf: &mut VecBuffer) -> Result<Self> {
match io_compression_codec() {
"lz4" => Ok(Self::LZ4(lz4_flex::frame::FrameEncoder::new(
shared_buf.writer(),
))),
"zstd" => {
let level = conf::SPARK_IO_COMPRESSION_ZSTD_LEVEL.value().unwrap_or(1);
let raw_encoder = raw::Encoder::new(level)?;
Ok(Self::ZSTD(Some(zio::Writer::new(
shared_buf.writer(),
raw_encoder,
))))
}
other => df_execution_err!("unsupported codec: {}", other),
}
}
}
impl Write for BlockCompressor {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
BlockCompressor::LZ4(w) => w.write(buf),
BlockCompressor::ZSTD(w) => w.as_mut().expect("zstd block writer missing").write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
BlockCompressor::LZ4(w) => w.flush(),
BlockCompressor::ZSTD(w) => w.as_mut().expect("zstd block writer missing").flush(),
}
}
}
pub struct IpcCompressionWriter<W: Write> {
output: W,
shared_buf: VecBuffer,
block_writer: BlockCompressor,
block_empty: bool,
}
unsafe impl<W: Write> Send for IpcCompressionWriter<W> {}
impl<W: Write> IpcCompressionWriter<W> {
pub fn new(output: W) -> Self {
let mut shared_buf = VecBuffer::default();
shared_buf.inner_mut().extend_from_slice(&[0u8; 4]);
let block_writer =
BlockCompressor::new(&mut shared_buf).expect("error creating compression encoder");
Self {
output,
shared_buf,
block_writer,
block_empty: true,
}
}
pub fn set_output(&mut self, output: W) {
assert!(
self.block_empty,
"IpcCompressionWriter must be empty while changing output"
);
self.output = output;
}
pub fn write_batch(&mut self, num_rows: usize, cols: &[ArrayRef]) -> Result<()> {
if num_rows == 0 {
return Ok(());
}
write_one_batch(num_rows, cols, &mut self.block_writer)?;
self.block_empty = false;
let buf_len = self.shared_buf.inner().len();
if buf_len as f64
>= conf::SHUFFLE_COMPRESSION_TARGET_BUF_SIZE
.value()
.unwrap_or(4194304) as f64 // default to 4MB
* 0.9
{
self.finish_current_buf()?;
}
Ok(())
}
pub fn finish_current_buf(&mut self) -> Result<()> {
if !self.block_empty {
// finish current block, reusing the underlying codec context where possible
let mut pending_zstd_encoder = None;
match &mut self.block_writer {
BlockCompressor::LZ4(w) => {
// try_finish() only closes the current lz4 frame, it does not consume
// the encoder, so the compression hash table can be reused as-is for
// the next frame.
w.try_finish()
.or_else(|_| df_execution_err!("ipc compression error"))?;
}
BlockCompressor::ZSTD(slot) => {
let mut w = slot.take().expect("zstd block writer missing");
w.finish()?;
let (_old_output, mut raw_encoder) = w.into_inner();
// reinit() is a cheap SessionOnly reset that prepares the CCtx for a
// new, independent frame without reallocating it.
raw_encoder.reinit()?;
pending_zstd_encoder = Some(raw_encoder);
}
}
// write
let block_len = self.shared_buf.inner().len() - 4;
self.shared_buf.inner_mut()[0..4]
.as_mut()
.write_u32::<LittleEndian>(block_len as u32)?;
self.output.write_all(self.shared_buf.inner())?;
// open next buf
self.shared_buf.inner_mut().clear();
self.shared_buf.inner_mut().extend_from_slice(&[0u8; 4]);
// lz4's FrameEncoder keeps writing to the same (now-cleared) shared buffer and
// will automatically start a new frame on next write(); zstd needs a fresh
// zio::Writer wrapping the reused raw_encoder and the new buffer.
if let Some(raw_encoder) = pending_zstd_encoder
&& let BlockCompressor::ZSTD(slot) = &mut self.block_writer
{
*slot = Some(zio::Writer::new(self.shared_buf.writer(), raw_encoder));
}
self.block_empty = true;
}
Ok(())
}
pub fn inner(&self) -> &W {
&self.output
}
pub fn inner_mut(&mut self) -> &mut W {
&mut self.output
}
}
/// Block-level decompressor used by [`IpcCompressionReader`]. Mirrors
/// [`BlockCompressor`] on the write side: unlike [`IoCompressionReader`]
/// (used for whole-stream decompression, e.g. spilling), this type is
/// designed so that the underlying codec context (lz4's decode buffers /
/// zstd's `DCtx`) can be reused across consecutive blocks instead of being
/// recreated from scratch every time a block is finished.
enum BlockDecompressor<R: Read> {
LZ4(lz4_flex::frame::FrameDecoder<Take<R>>),
ZSTD(ZstdBlockReader<R>),
}
impl<R: Read> BlockDecompressor<R> {
fn new(take: Take<R>) -> Result<Self> {
match io_compression_codec() {
"lz4" => Ok(Self::LZ4(lz4_flex::frame::FrameDecoder::new(take))),
"zstd" => Ok(Self::ZSTD(ZstdBlockReader::new(take)?)),
other => df_execution_err!("unsupported codec: {}", other),
}
}
/// Prepares the decompressor to read a new block of `block_len` bytes
/// from the same underlying reader, reusing the already-allocated codec
/// context where possible.
fn next_block(&mut self, block_len: u64) {
match self {
// lz4's FrameDecoder automatically parses a fresh frame header on the
// next read() once the previous frame's EndMark has been consumed, and
// reuses its internal src/dst buffers as-is, so only the `Take` limit
// needs to be reset here.
Self::LZ4(d) => d.get_mut().set_limit(block_len),
Self::ZSTD(d) => d.next_block(block_len),
}
}
/// Returns a mutable reference to the raw underlying reader, bypassing the
/// current block's `Take` limit; used to read the next block's length
/// prefix once the current block has been fully consumed.
fn inner_reader_mut(&mut self) -> &mut R {
match self {
Self::LZ4(d) => d.get_mut().get_mut(),
Self::ZSTD(d) => d.inner_reader_mut(),
}
}
}
impl<R: Read> Read for BlockDecompressor<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::LZ4(d) => d.read(buf),
Self::ZSTD(d) => d.read(buf),
}
}
}
enum ZstdReadState {
Reading,
PastEof,
Finished,
}
/// Minimal reimplementation of zstd's `zio::Reader`, specialized for reading
/// a single block bounded by a `Take<R>`. Unlike `zio::Reader` (whose
/// `into_inner()` only returns the wrapped reader and silently drops the
/// decode operation/context), this keeps the `raw::Decoder` (and therefore
/// its `DCtx`) as a directly-accessible field, so [`BlockDecompressor`] can
/// reuse it across block boundaries via [`ZstdBlockReader::next_block`]
/// instead of re-allocating a new `DCtx` for every block.
struct ZstdBlockReader<R: Read> {
reader: BufReader<Take<R>>,
decoder: raw::Decoder<'static>,
state: ZstdReadState,
finished_frame: bool,
}
impl<R: Read> ZstdBlockReader<R> {
fn new(take: Take<R>) -> Result<Self> {
Ok(Self {
reader: BufReader::new(take),
decoder: raw::Decoder::new()?,
state: ZstdReadState::Reading,
finished_frame: false,
})
}
fn next_block(&mut self, block_len: u64) {
self.reader.get_mut().set_limit(block_len);
self.state = ZstdReadState::Reading;
// `finished_frame` is intentionally left untouched: if the previous
// block completed normally it is already `true`, which makes
// the first read() of the new block lazily reinit() the DCtx (a
// cheap SessionOnly reset) before decoding, reusing the same
// context instead of allocating a new one.
}
fn inner_reader_mut(&mut self) -> &mut R {
self.reader.get_mut().get_mut()
}
}
impl<R: Read> Read for ZstdBlockReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut first = true;
loop {
match self.state {
ZstdReadState::Reading => {
let (bytes_read, bytes_written) = {
let input: &[u8] = if first { b"" } else { self.reader.fill_buf()? };
if !first && input.is_empty() {
self.state = ZstdReadState::PastEof;
continue;
}
first = false;
let mut src = raw::InBuffer::around(input);
let mut dst = raw::OutBuffer::around(buf);
if self.finished_frame && !input.is_empty() {
self.decoder.reinit()?;
self.finished_frame = false;
}
let hint = self.decoder.run(&mut src, &mut dst)?;
if hint == 0 {
self.finished_frame = true;
}
(src.pos(), dst.pos())
};
self.reader.consume(bytes_read);
if bytes_written > 0 {
return Ok(bytes_written);
}
}
ZstdReadState::PastEof => {
let mut dst = raw::OutBuffer::around(buf);
let hint = self.decoder.finish(&mut dst, self.finished_frame)?;
if hint == 0 {
self.state = ZstdReadState::Finished;
}
return Ok(dst.pos());
}
ZstdReadState::Finished => return Ok(0),
}
}
}
}
pub struct IpcCompressionReader<R: Read + 'static> {
input: InputState<R>,
}
unsafe impl<R: Read> Send for IpcCompressionReader<R> {}
#[derive(Default)]
enum InputState<R: Read + 'static> {
#[default]
Unreachable,
BlockStart(R),
BlockContent(BlockDecompressor<R>),
Finished,
}
impl<R: Read> IpcCompressionReader<R> {
pub fn new(input: R) -> Self {
Self {
input: InputState::BlockStart(input),
}
}
pub fn read_batch(&mut self, schema: &SchemaRef) -> Result<Option<(usize, Vec<ArrayRef>)>> {
struct Reader<'a, R: Read + 'static>(&'a mut IpcCompressionReader<R>);
impl<'a, R: Read> Read for Reader<'a, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match std::mem::take(&mut self.0.input) {
InputState::BlockStart(mut input) => {
let block_len = match input.read_u32::<LittleEndian>() {
Ok(block_len) => block_len,
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
self.0.input = InputState::Finished;
return Ok(0);
}
Err(err) => {
return Err(err);
}
};
let taken = input.take(block_len as u64);
self.0.input = InputState::BlockContent(BlockDecompressor::new(taken)?);
self.read(buf)
}
InputState::BlockContent(mut decompressor) => match decompressor.read(buf) {
Ok(len) if len > 0 => {
self.0.input = InputState::BlockContent(decompressor);
Ok(len)
}
Ok(_zero) => {
// current block fully consumed; try to move on to the next
// block while reusing the same decompressor (and therefore
// the same lz4/zstd codec context) instead of recreating it.
let next_block_len = {
let inner = decompressor.inner_reader_mut();
match inner.read_u32::<LittleEndian>() {
Ok(len) => Some(len),
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
None
}
Err(err) => return Err(err),
}
};
match next_block_len {
Some(len) => {
decompressor.next_block(len as u64);
self.0.input = InputState::BlockContent(decompressor);
self.read(buf)
}
None => {
self.0.input = InputState::Finished;
Ok(0)
}
}
}
Err(err) => Err(err),
},
InputState::Finished => Ok(0),
InputState::Unreachable => unreachable!(),
}
}
}
read_one_batch(&mut Reader(self), schema)
}
}
pub enum IoCompressionWriter<W: Write> {
LZ4(lz4_flex::frame::FrameEncoder<W>),
ZSTD(zstd::Encoder<'static, W>),
}
impl<W: Write> IoCompressionWriter<W> {
pub fn new_with_configured_codec(inner: W) -> Self {
Self::try_new(io_compression_codec(), inner).expect("error creating compression encoder")
}
pub fn try_new(codec: &str, inner: W) -> Result<Self> {
match codec {
"lz4" => Ok(Self::LZ4(lz4_flex::frame::FrameEncoder::new(inner))),
"zstd" => Ok(Self::ZSTD(zstd::Encoder::new(
inner,
conf::SPARK_IO_COMPRESSION_ZSTD_LEVEL.value().unwrap_or(1),
)?)),
_ => df_execution_err!("unsupported codec: {}", codec),
}
}
pub fn finish(mut self) -> Result<()> {
self.finish_internal()
}
fn finish_internal(&mut self) -> Result<()> {
match self {
IoCompressionWriter::LZ4(w) => {
w.try_finish()
.or_else(|_| df_execution_err!("ipc compression error"))?;
}
IoCompressionWriter::ZSTD(w) => {
w.do_finish()?;
}
}
Ok(())
}
}
impl<W: Write> Write for IoCompressionWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
IoCompressionWriter::LZ4(w) => w.write(buf),
IoCompressionWriter::ZSTD(w) => w.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
IoCompressionWriter::LZ4(w) => w.flush(),
IoCompressionWriter::ZSTD(w) => w.flush(),
}
}
}
pub enum IoCompressionReader<R: Read> {
LZ4(lz4_flex::frame::FrameDecoder<R>),
ZSTD(zstd::Decoder<'static, BufReader<R>>),
}
impl<R: Read> IoCompressionReader<R> {
pub fn new_with_configured_codec(inner: R) -> Self {
Self::try_new(io_compression_codec(), inner).expect("error creating compression encoder")
}
pub fn try_new(codec: &str, inner: R) -> Result<Self> {
match codec {
"lz4" => Ok(Self::LZ4(lz4_flex::frame::FrameDecoder::new(inner))),
"zstd" => Ok(Self::ZSTD(zstd::Decoder::new(inner)?)),
_ => df_execution_err!("unsupported codec: {}", codec),
}
}
pub fn finish_into_inner(self) -> Result<R> {
match self {
Self::LZ4(r) => Ok(r.into_inner()),
Self::ZSTD(r) => Ok(r.finish().into_inner()),
}
}
}
impl<R: Read> Read for IoCompressionReader<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::LZ4(r) => r.read(buf),
Self::ZSTD(r) => r.read(buf),
}
}
}
fn io_compression_codec() -> &'static str {
static CODEC: OnceCell<String> = OnceCell::new();
CODEC
.get_or_try_init(|| {
if is_jni_bridge_inited() {
conf::SPARK_IO_COMPRESSION_CODEC.value()
} else {
Ok("lz4".to_string()) // for testing
}
})
.expect("error reading spark.io.compression.codec")
.as_str()
}
#[derive(Default)]
struct VecBuffer {
vec: Box<Vec<u8>>,
}
struct VecBufferWrite {
unsafe_inner: *mut Vec<u8>,
}
impl Write for VecBufferWrite {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let inner = unsafe { &mut *self.unsafe_inner };
inner.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl VecBuffer {
fn inner(&self) -> &Vec<u8> {
&self.vec
}
fn inner_mut(&mut self) -> &mut Vec<u8> {
&mut self.vec
}
fn writer(&mut self) -> VecBufferWrite {
VecBufferWrite {
unsafe_inner: &mut *self.vec as *mut Vec<u8>,
}
}
}
#[cfg(test)]
mod tests {
use std::{error::Error, io::Cursor, sync::Arc};
use arrow::{
array::StringArray,
datatypes::{DataType, Field, Schema},
};
use super::*;
#[test]
fn test_ipc_compression() -> Result<(), Box<dyn Error>> {
let mut buf = vec![];
let mut writer = IpcCompressionWriter::new(&mut buf);
let test_array1: ArrayRef = Arc::new(StringArray::from(vec![Some("hello"), Some("world")]));
let test_array2: ArrayRef = Arc::new(StringArray::from(vec![Some("foo"), Some("bar")]));
let schema = Arc::new(Schema::new(vec![Field::new("", DataType::Utf8, false)]));
writer.write_batch(2, std::slice::from_ref(&test_array1))?;
// force finishing the first block before writing the second batch, so that the
// block-level compressor (lz4 FrameEncoder reused across blocks by default in
// tests) has to start a brand new frame for the second block.
writer.finish_current_buf()?;
writer.write_batch(2, std::slice::from_ref(&test_array2))?;
writer.finish_current_buf()?;
let mut reader = IpcCompressionReader::new(Cursor::new(buf));
let (num_rows1, arrays1) = reader.read_batch(&schema)?.expect("non-empty batch");
assert_eq!(num_rows1, 2);
assert_eq!(arrays1, &[test_array1]);
let (num_rows2, arrays2) = reader.read_batch(&schema)?.expect("non-empty batch");
assert_eq!(num_rows2, 2);
assert_eq!(arrays2, &[test_array2]);
assert!(reader.read_batch(&schema)?.is_none());
Ok(())
}
#[test]
fn test_zstd_cctx_reuse_across_frames() -> Result<(), Box<dyn Error>> {
let payload1 = b"the quick brown fox jumps over the lazy dog".repeat(16);
let payload2 = b"another totally different payload for the second frame".repeat(16);
let raw_encoder = raw::Encoder::new(1)?;
// frame 1
let mut buf1 = vec![];
let mut w = zio::Writer::new(&mut buf1, raw_encoder);
w.write_all(&payload1)?;
w.finish()?;
let (_, mut raw_encoder) = w.into_inner();
// reuse the same raw_encoder (and therefore the same CCtx) for an independent
// second frame, after a cheap reinit() instead of recreating the encoder.
raw_encoder.reinit()?;
let mut buf2 = vec![];
let mut w = zio::Writer::new(&mut buf2, raw_encoder);
w.write_all(&payload2)?;
w.finish()?;
let decoded1 = zstd::decode_all(Cursor::new(buf1))?;
let decoded2 = zstd::decode_all(Cursor::new(buf2))?;
assert_eq!(decoded1, payload1);
assert_eq!(decoded2, payload2);
Ok(())
}
#[test]
fn test_ipc_compression_many_blocks() -> Result<(), Box<dyn Error>> {
let mut buf = vec![];
let mut writer = IpcCompressionWriter::new(&mut buf);
let schema = Arc::new(Schema::new(vec![Field::new("", DataType::Utf8, false)]));
let arrays: Vec<ArrayRef> = (0..5)
.map(|i| {
Arc::new(StringArray::from(vec![
Some(format!("row{i}-a")),
Some(format!("row{i}-b")),
])) as ArrayRef
})
.collect();
for array in &arrays {
writer.write_batch(2, std::slice::from_ref(array))?;
// force a block boundary after every batch, exercising the decompressor's
// block-boundary reuse logic across many consecutive blocks.
writer.finish_current_buf()?;
}
let mut reader = IpcCompressionReader::new(Cursor::new(buf));
for array in &arrays {
let (num_rows, read_arrays) = reader.read_batch(&schema)?.expect("non-empty batch");
assert_eq!(num_rows, 2);
assert_eq!(&read_arrays, std::slice::from_ref(array));
}
assert!(reader.read_batch(&schema)?.is_none());
Ok(())
}
#[test]
fn test_zstd_block_reader_reuse_across_blocks() -> Result<(), Box<dyn Error>> {
let payloads = [
b"the quick brown fox jumps over the lazy dog".repeat(16),
b"another totally different payload for the second block".repeat(16),
b"a third, independent payload for the third block".repeat(16),
];
// encode each payload as an independent zstd frame, length-prefixed the same
// way `IpcCompressionWriter` frames its blocks.
let mut stream = vec![];
for payload in &payloads {
let raw_encoder = raw::Encoder::new(1)?;
let mut block_buf = vec![];
let mut w = zio::Writer::new(&mut block_buf, raw_encoder);
w.write_all(payload)?;
w.finish()?;
stream.write_u32::<LittleEndian>(block_buf.len() as u32)?;
stream.extend_from_slice(&block_buf);
}
let mut cursor = Cursor::new(stream);
let block_len = Read::by_ref(&mut cursor).read_u32::<LittleEndian>()?;
let take = Read::by_ref(&mut cursor).take(block_len as u64);
// only construct a single ZstdBlockReader (and therefore a single DCtx) for the
// first block; every subsequent block reuses it via `next_block`, mirroring
// what `BlockDecompressor::next_block` does for real multi-block IPC
// streams.
let mut block_reader = ZstdBlockReader::new(take)?;
for (i, payload) in payloads.iter().enumerate() {
let mut decoded = vec![];
block_reader.read_to_end(&mut decoded)?;
assert_eq!(&decoded, payload);
if i + 1 < payloads.len() {
let next_len = block_reader.inner_reader_mut().read_u32::<LittleEndian>()?;
block_reader.next_block(next_len as u64);
}
}
Ok(())
}
}