Skip to content

Release the GVL while decompressing in Zstd.decompress - #156

Open
sribalakumar wants to merge 1 commit into
SpringMT:mainfrom
sribalakumar:fix-decompress-gvl
Open

Release the GVL while decompressing in Zstd.decompress#156
sribalakumar wants to merge 1 commit into
SpringMT:mainfrom
sribalakumar:fix-decompress-gvl

Conversation

@sribalakumar

Copy link
Copy Markdown

Summary

Fixes #155.

The one-shot Zstd.decompress path holds Ruby's GVL throughout the libzstd call, blocking every other thread in the process. This routes it through the existing zstd_stream_decompress wrapper (already used by the streaming classes), which releases the GVL once per output chunk.

Change

In ext/zstdruby/zstdruby.c, function decode_one_frame:

-    size_t ret = ZSTD_decompressStream(dctx, &o, &in);
+    size_t ret = zstd_stream_decompress(dctx, &o, &in, false);

The false argument selects the GVL-releasing branch of the wrapper. The GVL is released and reacquired once per output chunk (ZSTD_DStreamOutSize, 128 KB) — the same granularity the streaming decompress path already uses, so other threads get a turn between chunks.

Why it's safe to release the GVL here

  • The scratch output buffer in decode_one_frame is malloc'd (ALLOC_N), not a Ruby String, so GC compaction cannot move it during the released window.
  • The input pointer comes from a stack-local VALUE, which is conservatively pinned by the GC.

So nothing the libzstd call touches can be relocated while the GVL is released. On very old Rubies without ruby/thread.h, the wrapper falls back to a direct call, so behaviour there is unchanged.

Verification

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.
@sribalakumar

Copy link
Copy Markdown
Author

@SpringMT Can you approve the pending workflow for the CI to run on this fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zstd.decompress holds the GVL, blocking other threads for the whole call

1 participant