zstd patch
This commit is contained in:
@@ -1,76 +0,0 @@
|
|||||||
Fix a 100% CPU worker spin in zstd-nginx-module 0.1.1 (tokers), triggered when a
|
|
||||||
compressible response is flushed mid-stream while being zstd-compressed.
|
|
||||||
|
|
||||||
Trigger: nginx injects an empty flush buffer via
|
|
||||||
ngx_http_send_special(r, NGX_HTTP_FLUSH) whenever the body is flushed without
|
|
||||||
new data -- `proxy_buffering off`, an upstream sending "X-Accel-Buffering: no",
|
|
||||||
Server-Sent Events / long-poll, FastCGI buffering off, or PHP flush()/ob_flush()
|
|
||||||
-- on a content type in zstd_types, to a client advertising
|
|
||||||
"Accept-Encoding: zstd" (Chrome 123+ and Firefox 126+ do by default).
|
|
||||||
|
|
||||||
Root cause: in ngx_http_zstd_filter_compress(), ZSTD_compressStream() returns a
|
|
||||||
*size hint* (the recommended next input size), not the amount of pending output,
|
|
||||||
so it is almost always > 0. The module misreads rc > 0 as "more output pending",
|
|
||||||
flips COMPRESS -> FLUSH and re-enters. ZSTD_flushStream() then returns 0 having
|
|
||||||
produced no bytes, the action is restored to COMPRESS, and ctx->flush is never
|
|
||||||
cleared (it is only cleared on the output-producing path). COMPRESS <-> FLUSH
|
|
||||||
then toggles forever: the worker pins one core at 100%, never returns to the
|
|
||||||
event loop (so a configuration reload cannot reap it) and survives until killed.
|
|
||||||
|
|
||||||
Fix: when a flush or end completes (ZSTD returned 0) without producing output,
|
|
||||||
clear ctx->flush / the GZIP_BUFFERED flag and stop instead of looping. No buffer
|
|
||||||
is manufactured -- a zero-length data buffer would be rejected by
|
|
||||||
ngx_http_write_filter ("zero size buf in writer"); the already-compressed data
|
|
||||||
has been emitted on the output-producing path, so there is nothing left to send.
|
|
||||||
|
|
||||||
Upstream tokers/zstd-nginx-module is effectively unmaintained (0.1.1 is the
|
|
||||||
latest tag, 2023-10-23), so this is carried as a local patch.
|
|
||||||
|
|
||||||
Verified against vanilla nginx 1.31.2 + module 0.1.1: unpatched pins a worker at
|
|
||||||
100% CPU (state R) on an unbuffered flushed response and never recovers; patched
|
|
||||||
keeps the worker idle (state S) and the streamed response decodes to a complete,
|
|
||||||
valid zstd frame (zstd -t: FRAME OK).
|
|
||||||
|
|
||||||
--- a/filter/ngx_http_zstd_filter_module.c 2026-06-30 20:51:33.329153852 +0000
|
|
||||||
+++ b/filter/ngx_http_zstd_filter_module.c 2026-06-30 21:05:09.666179333 +0000
|
|
||||||
@@ -460,6 +460,40 @@
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ngx_buf_size(ctx->out_buf) == 0) {
|
|
||||||
+
|
|
||||||
+ if (rc == 0 && (ctx->flush || ctx->last)) {
|
|
||||||
+ /*
|
|
||||||
+ * twiy/raweb fix for the zstd-nginx-module 0.1.1 worker CPU spin.
|
|
||||||
+ *
|
|
||||||
+ * A flush or end has just completed (ZSTD_flushStream()/
|
|
||||||
+ * ZSTD_endStream() returned 0) but produced no output: the zstd
|
|
||||||
+ * stream buffer was already drained, so there is nothing left to
|
|
||||||
+ * emit downstream. Clear the pending flush bookkeeping here instead
|
|
||||||
+ * of looping; do NOT manufacture a buffer (a zero-length data buf
|
|
||||||
+ * is rejected by ngx_http_write_filter with "zero size buf in
|
|
||||||
+ * writer").
|
|
||||||
+ *
|
|
||||||
+ * Without this an empty flush buffer -- injected by nginx via
|
|
||||||
+ * ngx_http_send_special(NGX_HTTP_FLUSH): proxy_buffering off,
|
|
||||||
+ * "X-Accel-Buffering: no", SSE, FastCGI flush, PHP flush() --
|
|
||||||
+ * wedges the worker at 100% CPU forever. ZSTD_compressStream()
|
|
||||||
+ * returns a non-zero *size hint* (recommended next input size, not
|
|
||||||
+ * pending output) that is misread as "redo", flipping
|
|
||||||
+ * COMPRESS->FLUSH; ZSTD_flushStream() then returns 0 with no
|
|
||||||
+ * output; ctx->flush is only cleared on the output-producing path
|
|
||||||
+ * below, which is never reached, so COMPRESS<->FLUSH toggles
|
|
||||||
+ * indefinitely.
|
|
||||||
+ */
|
|
||||||
+
|
|
||||||
+ r->connection->buffered &= ~NGX_HTTP_GZIP_BUFFERED;
|
|
||||||
+ ctx->flush = 0;
|
|
||||||
+
|
|
||||||
+ if (ctx->last) {
|
|
||||||
+ ctx->done = 1;
|
|
||||||
+ return NGX_OK;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
return NGX_AGAIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
+10
-19
@@ -244,27 +244,18 @@ function clean_install() {
|
|||||||
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NGX_MOD_ZSTD — Zstandard compression module from tokers. Pinned via
|
# NGX_MOD_ZSTD — Zstandard compression module from eilandert (maintained
|
||||||
# NGX_MOD_ZSTD; tarball pattern (dir name embeds version → cache invalidates
|
# fork of tokers/zstd-nginx-module, which has been abandoned since 0.1.1 in
|
||||||
# automatically when the pin moves).
|
# 2023-10-23). Pinned via NGX_MOD_ZSTD; tarball pattern (dir name embeds
|
||||||
|
# version → cache invalidates automatically when the pin moves).
|
||||||
|
#
|
||||||
|
# The fork ships the flush-loop CPU-spin fix we previously carried as
|
||||||
|
# build/patches/zstd-nginx-module-0.1.1-flush-cpu-spin.patch — see PR #23,
|
||||||
|
# PR #49, and the "content-less flush completed" branch in
|
||||||
|
# filter/ngx_http_zstd_filter_module.c. No local patching needed.
|
||||||
if [ ! -d /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} ]; then
|
if [ ! -d /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} ]; then
|
||||||
cd /opt/mod/; wget https://github.com/tokers/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
cd /opt/mod/; wget https://github.com/eilandert/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
||||||
cd /opt/mod/; tar xf ${NGX_MOD_ZSTD}.tar.gz; rm -Rf ${NGX_MOD_ZSTD}.tar.gz
|
cd /opt/mod/; tar xf ${NGX_MOD_ZSTD}.tar.gz; rm -Rf ${NGX_MOD_ZSTD}.tar.gz
|
||||||
|
|
||||||
# Local fix for the zstd-nginx-module 0.1.1 worker CPU spin (100% CPU on
|
|
||||||
# a flushed compressible response: proxy_buffering off, SSE, PHP flush()).
|
|
||||||
# Upstream is unmaintained (0.1.1 is the latest tag), so we carry it.
|
|
||||||
# Applied here rather than in apply_patches() because the module lives
|
|
||||||
# outside the nginx source tree; the [ ! -d ] guard above makes this a
|
|
||||||
# fresh-extract-only operation (rm the module dir to force a re-patch).
|
|
||||||
zstd_fix="${SCRIPT_DIR}/patches/zstd-nginx-module-${NGX_MOD_ZSTD}-flush-cpu-spin.patch"
|
|
||||||
if [ "${APPLY_PATCH_ZSTD_FLUSH_FIX:-1}" = "1" ]; then
|
|
||||||
[ -f "$zstd_fix" ] || { echo "[patch] MISSING $zstd_fix"; exit 1; }
|
|
||||||
echo "[patch] applying zstd-nginx-module-${NGX_MOD_ZSTD}-flush-cpu-spin.patch"
|
|
||||||
( cd /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} && patch -p1 < "$zstd_fix" )
|
|
||||||
else
|
|
||||||
echo "[patch] skip zstd flush fix (APPLY_PATCH_ZSTD_FLUSH_FIX=0)"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# END OF NGINX MODULES
|
# END OF NGINX MODULES
|
||||||
|
|||||||
+10
-19
@@ -244,27 +244,18 @@ function clean_install() {
|
|||||||
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NGX_MOD_ZSTD — Zstandard compression module from tokers. Pinned via
|
# NGX_MOD_ZSTD — Zstandard compression module from eilandert (maintained
|
||||||
# NGX_MOD_ZSTD; tarball pattern (dir name embeds version → cache invalidates
|
# fork of tokers/zstd-nginx-module, which has been abandoned since 0.1.1 in
|
||||||
# automatically when the pin moves).
|
# 2023-10-23). Pinned via NGX_MOD_ZSTD; tarball pattern (dir name embeds
|
||||||
|
# version → cache invalidates automatically when the pin moves).
|
||||||
|
#
|
||||||
|
# The fork ships the flush-loop CPU-spin fix we previously carried as
|
||||||
|
# build/patches/zstd-nginx-module-0.1.1-flush-cpu-spin.patch — see PR #23,
|
||||||
|
# PR #49, and the "content-less flush completed" branch in
|
||||||
|
# filter/ngx_http_zstd_filter_module.c. No local patching needed.
|
||||||
if [ ! -d /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} ]; then
|
if [ ! -d /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} ]; then
|
||||||
cd /opt/mod/; wget https://github.com/tokers/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
cd /opt/mod/; wget https://github.com/eilandert/zstd-nginx-module/archive/refs/tags/${NGX_MOD_ZSTD}.tar.gz
|
||||||
cd /opt/mod/; tar xf ${NGX_MOD_ZSTD}.tar.gz; rm -Rf ${NGX_MOD_ZSTD}.tar.gz
|
cd /opt/mod/; tar xf ${NGX_MOD_ZSTD}.tar.gz; rm -Rf ${NGX_MOD_ZSTD}.tar.gz
|
||||||
|
|
||||||
# Local fix for the zstd-nginx-module 0.1.1 worker CPU spin (100% CPU on
|
|
||||||
# a flushed compressible response: proxy_buffering off, SSE, PHP flush()).
|
|
||||||
# Upstream is unmaintained (0.1.1 is the latest tag), so we carry it.
|
|
||||||
# Applied here rather than in apply_patches() because the module lives
|
|
||||||
# outside the nginx source tree; the [ ! -d ] guard above makes this a
|
|
||||||
# fresh-extract-only operation (rm the module dir to force a re-patch).
|
|
||||||
zstd_fix="${SCRIPT_DIR}/patches/zstd-nginx-module-${NGX_MOD_ZSTD}-flush-cpu-spin.patch"
|
|
||||||
if [ "${APPLY_PATCH_ZSTD_FLUSH_FIX:-1}" = "1" ]; then
|
|
||||||
[ -f "$zstd_fix" ] || { echo "[patch] MISSING $zstd_fix"; exit 1; }
|
|
||||||
echo "[patch] applying zstd-nginx-module-${NGX_MOD_ZSTD}-flush-cpu-spin.patch"
|
|
||||||
( cd /opt/mod/zstd-nginx-module-${NGX_MOD_ZSTD} && patch -p1 < "$zstd_fix" )
|
|
||||||
else
|
|
||||||
echo "[patch] skip zstd flush fix (APPLY_PATCH_ZSTD_FLUSH_FIX=0)"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# END OF NGINX MODULES
|
# END OF NGINX MODULES
|
||||||
|
|||||||
@@ -54,10 +54,16 @@ export NGX_MOD_LUA_LOCK="0.09"
|
|||||||
# https://github.com/openresty/srcache-nginx-module/tags
|
# https://github.com/openresty/srcache-nginx-module/tags
|
||||||
export NGX_MOD_LUA_SRCACHE="0.33"
|
export NGX_MOD_LUA_SRCACHE="0.33"
|
||||||
|
|
||||||
# https://github.com/tokers/zstd-nginx-module/tags
|
# https://github.com/eilandert/zstd-nginx-module/tags
|
||||||
# Zstandard compression module. Chrome 123+ and Firefox 126+ send
|
# Zstandard compression module. Chrome 123+ and Firefox 126+ send
|
||||||
# `Accept-Encoding: zstd`; older clients fall back to brotli/gzip.
|
# `Accept-Encoding: zstd`; older clients fall back to brotli/gzip.
|
||||||
export NGX_MOD_ZSTD="0.1.1"
|
# Switched from tokers/zstd-nginx-module (unmaintained since 0.1.1, 2023-10-23)
|
||||||
|
# to eilandert/zstd-nginx-module, an actively maintained fork that has already
|
||||||
|
# absorbed the flush-loop CPU-spin fix we previously carried as a local patch
|
||||||
|
# (PR #23 + PR #49 + follow-up work — see filter/ngx_http_zstd_filter_module.c
|
||||||
|
# in the fork for the "content-less flush completed" branch that supersedes
|
||||||
|
# our patch).
|
||||||
|
export NGX_MOD_ZSTD="0.90.6"
|
||||||
|
|
||||||
# https://github.com/zlib-ng/zlib-ng/releases
|
# https://github.com/zlib-ng/zlib-ng/releases
|
||||||
# Drop-in libz replacement with SIMD-accelerated DEFLATE. Built in --zlib-compat
|
# Drop-in libz replacement with SIMD-accelerated DEFLATE. Built in --zlib-compat
|
||||||
|
|||||||
Reference in New Issue
Block a user