Parallel compression and bsdiff#3588
Conversation
|
Hi @jorisdobbelsteen. Thanks for your PR. I'm waiting for a ostreedev member to verify that this patch is reasonable to test. If it is, they should reply with Regular contributors should join the org to skip this step. Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
There was a problem hiding this comment.
Code Review
This pull request introduces parallel compression and bsdiff processing for static delta generation in libostree. It adds a new --threads option to the static-delta generate command, allowing users to specify the number of worker threads or use keywords like 'auto' to utilize all available CPUs. The implementation introduces thread pools for both compression and bsdiff tasks, ensuring that the output remains byte-identical to the serial path. The review identified potential race conditions in error propagation and suggested improvements for fail-fast behavior in the bsdiff task submission process.
| g_cond_wait (&cp->cond, &cp->mutex); | ||
| if (cp->first_error != NULL) | ||
| { | ||
| g_propagate_error (error, g_steal_pointer (&cp->first_error)); |
There was a problem hiding this comment.
Using g_steal_pointer here introduces a race condition. If the producer thread is woken up by a worker's failure, it steals the error from the pool, making cp->first_error NULL. If the producer then continues and calls submit_finish_part again (e.g., for the final part), it will not see the error and may incorrectly submit more work. This can lead to unpredictable behavior in error scenarios.
You should use g_error_copy to propagate the error without clearing the error state from the pool. This ensures that subsequent checks against cp->first_error will correctly detect the failed state.
g_propagate_error (error, g_error_copy (cp->first_error));There was a problem hiding this comment.
Thanks. There's no data race here: first_error is only read/written under cp->mutex, there's a single producer thread, and every caller of submit_finish_part() returns immediately on the first failure — so the "producer continues and submits the final part" scenario can't occur, and drain_compress_pool() is never reached on that path (no double-report).
That said, the change pointed at a real smell: g_steal_pointer consumed the error and cleared the "pool has failed" flag that queued workers rely on to short-circuit. I've reworked it so the pool owns first_error for its lifetime and propagates a copy (freed once in worker_pool_clear()), which is what the bsdiff pool already did — so both pools are now consistent and the flag stays valid. Done in submit_finish_part() and drain_compress_pool(), with comments explaining the ownership model.
| g_mutex_lock (&bp->mutex); | ||
| bp->inflight++; | ||
| g_mutex_unlock (&bp->mutex); |
There was a problem hiding this comment.
This section could be improved to fail-fast if an error has already occurred in another worker. Currently, the producer will continue to enqueue bsdiff tasks even if the pool is in a failed state. While workers will short-circuit, stopping the producer earlier is more efficient and consistent with the error handling in the compression pool.
Consider checking for bp->first_error before incrementing inflight and pushing to the pool.
g_mutex_lock (&bp->mutex);
if (bp->first_error)
{
g_propagate_error (error, g_error_copy (bp->first_error));
g_mutex_unlock (&bp->mutex);
return FALSE;
}
bp->inflight++;
g_mutex_unlock (&bp->mutex);There was a problem hiding this comment.
Applied — submit_bsdiff_task() now checks bp->first_error under the mutex before incrementing inflight and returns early (propagating a copy) if another worker has already failed, mirroring submit_finish_part(). The already-submitted tasks are still reaped by drain_bsdiff_pool() on the error path.
Static delta generation spends most of its time in LZMA part compression and in bsdiff patch computation. Both are CPU-bound and were previously run serially on a single core. Add a --threads option to `ostree static-delta generate` (also settable through the GVariant parameters and the OSTREE_DELTA_COMPRESS_THREADS environment variable; "auto" uses all available CPUs). When enabled, LZMA part compression and bsdiff patch computation run across a pool of worker threads. A single producer thread drives submission and keeps the output ordering identical to the serial path, so the generated delta is byte-for-byte identical regardless of the thread count. Worker errors are surfaced through a mutex-protected first_error on each pool; the producer fails fast and every in-flight worker is drained before any shared task state is freed. Verified with the address, undefined-behavior and thread sanitizers; output is byte-identical to the serial path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
01ea5d9 to
cbd742a
Compare
|
Pushed an updated revision. Summary of changes since the last push:
Design note for maintainers: this introduces Scope confirmation: no public API/ABI change — no installed header or The remaining red checks are unrelated CI-infra failures (SIGSEGV during the CoreOS-buildroot cargo/dnf setup, and apt/packit flakes) — the jobs that actually build this change ( |
I have a similar performance desire as in issue #2845, except for the compression part.
This change adds a way that bsdiff and lzma compression for static-delta can be run on multiple cores.
Verification was done with sanitisers, and the thread sanitiser; the output should be byte identical.
Looking for input on this feature, since I uses claude and before I invest significant time for further clean up of any code.