From 12c3cd292dd8b8a0df327f9e8ff8ec084ff6bec8 Mon Sep 17 00:00:00 2001 From: Bala Kumar Date: Tue, 1 Sep 2026 11:27:27 +0530 Subject: [PATCH] Release the GVL while decompressing in Zstd.decompress Zstd.decompress runs its decode loop by calling ZSTD_decompressStream directly, so it holds the GVL for the whole operation. Zstd.compress and both streaming paths already go through the rb_thread_call_without_gvl wrappers in common.h; decompress is the one hot path that does not, so a large decompress blocks every other thread in the process until it finishes. Route the decode loop through the existing zstd_stream_decompress wrapper with gvl=false, matching the streaming decompress path. The GVL is released and reacquired once per output chunk (ZSTD_DStreamOutSize), so other threads run between chunks. This is safe here: the scratch buffer is malloc'd (ALLOC_N), not a movable Ruby String, and the input pointer comes from a stack-pinned VALUE, so nothing the call touches can move while the GVL is released. --- ext/zstdruby/zstdruby.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/zstdruby/zstdruby.c b/ext/zstdruby/zstdruby.c index 924e9bb..c0cc17f 100644 --- a/ext/zstdruby/zstdruby.c +++ b/ext/zstdruby/zstdruby.c @@ -52,7 +52,7 @@ static VALUE decode_one_frame(ZSTD_DCtx* dctx, const unsigned char* src, size_t for (;;) { ZSTD_outBuffer o = (ZSTD_outBuffer){ buf, cap, 0 }; size_t const in_pos_before = in.pos; - size_t ret = ZSTD_decompressStream(dctx, &o, &in); + size_t ret = zstd_stream_decompress(dctx, &o, &in, false); if (ZSTD_isError(ret)) { xfree(buf); rb_raise(rb_eRuntimeError, "ZSTD_decompressStream failed: %s", ZSTD_getErrorName(ret));