feat(compress): move MT job orchestration into Rust

Move the high-level ZSTDMT compression-job stage sequence into Rust: resource
acquisition, per-job parameter preparation, serial sequence handling, context
initialization, external-sequence application, non-first frame-header repair,
chunk compression/error routing, tracing, and common finalization. Keep C-owned
job descriptors, pools, synchronization, codec contexts, serial state, and
cleanup behind callbacks so private worker state does not cross the boundary.

Test Plan:
- Rust library all-target tests: 517 passed, including MT job-order tests.
- Rust legacy feature matrix: 572 passed.
- Rust and CLI clippy, nightly fmt, native CLI tests (41), and library smoke.
- Native test-zstd, bounded fuzzer (319), zstream (152 + 297), and decode
  corpus (1,647) all passed, including multi-GiB and MT round trips.
- All heavy checks ran serially with CARGO_BUILD_JOBS=1 or make -j1 and
  ulimit -v 41943040 (40 GiB virtual memory).
- Commit is intentionally unsigned because configured GPG pinentry was
  unavailable and hung during the signing attempt.
This commit is contained in:
2026-07-19 09:14:50 +02:00
parent 47b0da7ad7
commit 1655302473
2 changed files with 493 additions and 80 deletions
+295
View File
@@ -56,6 +56,25 @@ pub struct ZSTDMT_chunkProcessResult {
pub lastBlockSize: usize,
}
/// Scalar job state used by the Rust compression-job scheduler.
///
/// The job descriptor, pools, synchronization, and codec state remain
/// private to C. Rust uses only the frame-position flags to choose the
/// sequencing and non-first-job header stages.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZSTDMT_compressionJobProjection {
pub firstJob: c_uint,
pub lastJob: c_uint,
}
pub type ZSTDMT_compressionJobStepFn = unsafe extern "C" fn(*mut c_void) -> usize;
pub type ZSTDMT_compressionJobVoidFn = unsafe extern "C" fn(*mut c_void);
pub type ZSTDMT_compressionJobCompressFn =
unsafe extern "C" fn(*mut c_void, c_uint) -> ZSTDMT_chunkProcessResult;
pub type ZSTDMT_compressionJobErrorFn = unsafe extern "C" fn(*mut c_void, usize);
pub type ZSTDMT_compressionJobFinishFn = unsafe extern "C" fn(*mut c_void, usize);
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZSTDMT_flushPublicationResult {
@@ -560,6 +579,135 @@ pub unsafe extern "C" fn ZSTDMT_rust_createCompressionJob(
)
}
/// Run the high-level worker-job sequence while C owns all codec and
/// synchronization operations behind callbacks.
///
/// Resource acquisition and every codec-facing stage can fail with a zstd
/// error. Rust stops at the first such error, reports it before the common
/// C-owned cleanup callback, and passes the final block size only on the
/// successful compression path.
#[inline]
fn compression_job_with<A, P, S, B, Q, H, C, T, E, F>(
projection: ZSTDMT_compressionJobProjection,
mut acquire_resources: A,
mut prepare_parameters: P,
mut generate_sequences: S,
mut begin_job: B,
mut apply_sequences: Q,
mut write_frame_header: H,
mut compress_job: C,
mut trace_job: T,
mut set_error: E,
mut finish_job: F,
) where
A: FnMut() -> usize,
P: FnMut(),
S: FnMut(),
B: FnMut() -> usize,
Q: FnMut(),
H: FnMut() -> usize,
C: FnMut(c_uint) -> ZSTDMT_chunkProcessResult,
T: FnMut(),
E: FnMut(usize),
F: FnMut(usize),
{
let mut error = acquire_resources();
let mut last_block_size = 0;
if !ERR_isError(error) {
prepare_parameters();
generate_sequences();
error = begin_job();
}
if !ERR_isError(error) {
apply_sequences();
if projection.firstJob == 0 {
error = write_frame_header();
}
}
if !ERR_isError(error) {
let result = compress_job(projection.lastJob);
if ERR_isError(result.error) {
error = result.error;
} else {
last_block_size = result.lastBlockSize;
trace_job();
}
}
if ERR_isError(error) {
set_error(error);
last_block_size = 0;
}
finish_job(last_block_size);
}
/// C ABI entry point for the worker-job orchestration. C supplies callbacks
/// that keep the private descriptor, pools, mutexes, and codec operations on
/// the C side of this narrow projection.
#[cfg(not(test))]
#[no_mangle]
pub unsafe extern "C" fn ZSTDMT_rust_compressionJob(
projection: *const ZSTDMT_compressionJobProjection,
opaque: *mut c_void,
acquireResources: Option<ZSTDMT_compressionJobStepFn>,
prepareParameters: Option<ZSTDMT_compressionJobVoidFn>,
generateSequences: Option<ZSTDMT_compressionJobVoidFn>,
beginJob: Option<ZSTDMT_compressionJobStepFn>,
applySequences: Option<ZSTDMT_compressionJobVoidFn>,
writeFrameHeader: Option<ZSTDMT_compressionJobStepFn>,
compressJob: Option<ZSTDMT_compressionJobCompressFn>,
traceJob: Option<ZSTDMT_compressionJobVoidFn>,
setError: Option<ZSTDMT_compressionJobErrorFn>,
finishJob: Option<ZSTDMT_compressionJobFinishFn>,
) {
let Some(projection) = (unsafe { projection.as_ref() }).copied() else {
return;
};
let (
Some(acquire_resources),
Some(prepare_parameters),
Some(generate_sequences),
Some(begin_job),
Some(apply_sequences),
Some(write_frame_header),
Some(compress_job),
Some(trace_job),
Some(set_error),
Some(finish_job),
) = (
acquireResources,
prepareParameters,
generateSequences,
beginJob,
applySequences,
writeFrameHeader,
compressJob,
traceJob,
setError,
finishJob,
)
else {
return;
};
compression_job_with(
projection,
|| unsafe { acquire_resources(opaque) },
|| unsafe { prepare_parameters(opaque) },
|| unsafe { generate_sequences(opaque) },
|| unsafe { begin_job(opaque) },
|| unsafe { apply_sequences(opaque) },
|| unsafe { write_frame_header(opaque) },
|last_job| unsafe { compress_job(opaque, last_job) },
|| unsafe { trace_job(opaque) },
|error| unsafe { set_error(opaque, error) },
|last_block_size| unsafe { finish_job(opaque, last_block_size) },
);
}
#[inline]
fn invalid_flush_publication(
output_pos: usize,
@@ -2556,6 +2704,7 @@ pub unsafe extern "C" fn ZSTDMT_rust_cctx_pool_release(pool: *mut RustCCtxPool,
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::{cell::RefCell, rc::Rc};
const DEFAULT_MEM: ZstdCustomMem = ZstdCustomMem {
customAlloc: None,
@@ -2580,6 +2729,152 @@ mod tests {
calls: Vec<(usize, usize)>,
}
#[derive(Default)]
struct MockCompressionJob {
events: Vec<&'static str>,
errors: Vec<usize>,
finished: Vec<usize>,
}
fn record_compression_job_event(state: &Rc<RefCell<MockCompressionJob>>, event: &'static str) {
state.borrow_mut().events.push(event);
}
#[test]
fn compression_job_runs_first_job_stages_and_reports_final_block() {
let state = Rc::new(RefCell::new(MockCompressionJob::default()));
let acquire_state = Rc::clone(&state);
let prepare_state = Rc::clone(&state);
let sequence_state = Rc::clone(&state);
let begin_state = Rc::clone(&state);
let apply_state = Rc::clone(&state);
let compress_state = Rc::clone(&state);
let trace_state = Rc::clone(&state);
let finish_state = Rc::clone(&state);
compression_job_with(
ZSTDMT_compressionJobProjection {
firstJob: 1,
lastJob: 1,
},
move || {
record_compression_job_event(&acquire_state, "acquire");
0
},
move || record_compression_job_event(&prepare_state, "prepare"),
move || record_compression_job_event(&sequence_state, "sequences"),
move || {
record_compression_job_event(&begin_state, "begin");
0
},
move || record_compression_job_event(&apply_state, "apply"),
|| panic!("first jobs do not rewrite a frame header"),
move |last_job| {
assert_eq!(last_job, 1);
record_compression_job_event(&compress_state, "compress");
ZSTDMT_chunkProcessResult {
error: 0,
lastBlockSize: 7,
}
},
move || record_compression_job_event(&trace_state, "trace"),
|_error| panic!("success must not report an error"),
move |last_block_size| {
record_compression_job_event(&finish_state, "finish");
finish_state.borrow_mut().finished.push(last_block_size);
},
);
let state = state.borrow();
assert_eq!(
state.events,
vec![
"acquire",
"prepare",
"sequences",
"begin",
"apply",
"compress",
"trace",
"finish"
]
);
assert!(state.errors.is_empty());
assert_eq!(state.finished, vec![7]);
}
#[test]
fn compression_job_stops_on_non_first_chunk_error_and_cleans_up() {
let state = Rc::new(RefCell::new(MockCompressionJob::default()));
let acquire_state = Rc::clone(&state);
let prepare_state = Rc::clone(&state);
let sequence_state = Rc::clone(&state);
let begin_state = Rc::clone(&state);
let apply_state = Rc::clone(&state);
let header_state = Rc::clone(&state);
let compress_state = Rc::clone(&state);
let error_state = Rc::clone(&state);
let finish_state = Rc::clone(&state);
let expected_error = ERROR(ZstdErrorCode::DstSizeTooSmall);
compression_job_with(
ZSTDMT_compressionJobProjection {
firstJob: 0,
lastJob: 1,
},
move || {
record_compression_job_event(&acquire_state, "acquire");
0
},
move || record_compression_job_event(&prepare_state, "prepare"),
move || record_compression_job_event(&sequence_state, "sequences"),
move || {
record_compression_job_event(&begin_state, "begin");
0
},
move || record_compression_job_event(&apply_state, "apply"),
move || {
record_compression_job_event(&header_state, "header");
0
},
move |last_job| {
assert_eq!(last_job, 1);
record_compression_job_event(&compress_state, "compress");
ZSTDMT_chunkProcessResult {
error: expected_error,
lastBlockSize: 0,
}
},
|| panic!("a failed chunk must not be traced"),
move |error| {
record_compression_job_event(&error_state, "error");
error_state.borrow_mut().errors.push(error);
},
move |last_block_size| {
record_compression_job_event(&finish_state, "finish");
finish_state.borrow_mut().finished.push(last_block_size);
},
);
let state = state.borrow();
assert_eq!(
state.events,
vec![
"acquire",
"prepare",
"sequences",
"begin",
"apply",
"header",
"compress",
"error",
"finish"
]
);
assert_eq!(state.errors, vec![expected_error]);
assert_eq!(state.finished, vec![0]);
}
unsafe extern "C" fn mock_compress_continue(
cctx: *mut c_void,
_dst: *mut c_void,