diff --git a/build/patches/zstd-nginx-module-0.1.1-flush-cpu-spin.patch b/build/patches/zstd-nginx-module-0.1.1-flush-cpu-spin.patch deleted file mode 100644 index d99f96c..0000000 --- a/build/patches/zstd-nginx-module-0.1.1-flush-cpu-spin.patch +++ /dev/null @@ -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; - } - diff --git a/build/raccoon.sh b/build/raccoon.sh index 95e05a7..a530baa 100644 --- a/build/raccoon.sh +++ b/build/raccoon.sh @@ -244,27 +244,18 @@ function clean_install() { cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi fi - # NGX_MOD_ZSTD — Zstandard compression module from tokers. Pinned via - # NGX_MOD_ZSTD; tarball pattern (dir name embeds version → cache invalidates - # automatically when the pin moves). + # NGX_MOD_ZSTD — Zstandard compression module from eilandert (maintained + # fork of tokers/zstd-nginx-module, which has been abandoned since 0.1.1 in + # 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 - 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 - - # 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 # END OF NGINX MODULES diff --git a/build/trixie.sh b/build/trixie.sh index 3c8e4ed..41d96e1 100644 --- a/build/trixie.sh +++ b/build/trixie.sh @@ -244,27 +244,18 @@ function clean_install() { cd /opt/mod/; git clone --recurse-submodules https://github.com/wargio/naxsi.git naxsi fi - # NGX_MOD_ZSTD — Zstandard compression module from tokers. Pinned via - # NGX_MOD_ZSTD; tarball pattern (dir name embeds version → cache invalidates - # automatically when the pin moves). + # NGX_MOD_ZSTD — Zstandard compression module from eilandert (maintained + # fork of tokers/zstd-nginx-module, which has been abandoned since 0.1.1 in + # 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 - 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 - - # 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 # END OF NGINX MODULES diff --git a/version b/version index 1c4ff8f..3967cc9 100644 --- a/version +++ b/version @@ -54,10 +54,16 @@ export NGX_MOD_LUA_LOCK="0.09" # https://github.com/openresty/srcache-nginx-module/tags 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 # `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 # Drop-in libz replacement with SIMD-accelerated DEFLATE. Built in --zlib-compat