Changes with the nginx-cache-turbo-module 1.1.0                 17 Aug 2026

    *) Security: invalid or out-of-range Redis database suffixes no longer
       echo a credential-bearing DSN into nginx configuration-error logs.

    *) Bugfix: a memcached connection was treated as proven after send()
       accepted the command locally. A peer that closed without replying
       therefore failed to arm connect_backoff and was redialled on every
       miss. The connection is now proven only by the first reply byte.

    *) Bugfix: Redis GET and fire-and-forget reply handlers now require an
       exact RESP boundary before accepting a payload or pooling a keepalive
       connection. Malformed payload delimiters and trailing partial replies
       close the connection instead of desynchronising its next reuse.

    *) Feature: cache_turbo_bypass_stale_uri can retain selected public,
       normally-bypassed responses solely as circuit-breaker fallback. These
       entries remain unreachable as ordinary HIT or STALE responses.

    *) Bugfix: stale-if-error can rescue a response when the origin fails in
       the middle of its body before any bytes have been flushed downstream.
       Live stale-if-error entries also get a second-chance eviction while
       their circuit breaker is open.

    *) Bugfix: over-long cache tags now emit a configuration warning instead
       of being dropped silently, and L2 blob storage no longer reads an
       uninitialised stack field while assembling metadata.

    *) Performance: bounded Cookie classification removes attacker-controlled
       scan work, query-argument classification no longer rescans after 64
       spans, cache-key cookie folding uses one allocation, and the Redis SCAN
       framer resumes linearly across partial reads.

    *) Maintenance: decomposed the configuration, request, restore, classifier,
       Redis and admin paths into smaller helpers without changing their public
       directives; expanded unit, fuzz, preset-parity and runtime regressions
       around the resulting seams.

    *) Bugfix: a ?all=1 L2 all-purge's SCAN walk had only a per-page bound
       (each page's read re-arms the read timeout), never a bound on the
       whole walk. A backend that always returned a non-zero cursor just
       under that timeout could park the purge request for up to the fixed
       page cap's worth of pages -- hours, not seconds. cache_turbo_redis now
       accepts scan_deadline= (default 30s), a wall-clock ceiling checked at
       every page boundary alongside the page cap; 0 disables it (page-cap
       only, prior behaviour). A walk abandoned by the deadline reports the
       purge INCOMPLETE the same way the page cap does, with
       "reason":"deadline" distinguishing it from "reason":"page-cap".

    *) BEHAVIOUR CHANGE: cache_turbo_auto_vary now defaults to on. While it
       defaulted to off, the module ignored the response's own Vary header,
       which left the Vary: Cookie / Authorization veto as dead code: a
       per-user response was stored under a Vary-blind key and served to
       every client, and the stale-serve path widened that window. On by
       default, an un-split Vary axis can no longer serve the wrong
       representation. An origin that emits Vary: Cookie on cacheable public
       pages (Django/Wagtail is the common case) must now set
       cache_turbo_auto_vary off explicitly to keep caching them -- see
       docs/wagtail.md.

    *) Bugfix: a PURGE that reached the Redis variant-index walk leaked the
       client connection. The request was parked with a reference that
       nothing released, because the PRECONTENT phase handler returning
       NGX_DONE never reaches ngx_http_finalize_request. The connection
       stayed open with its fd held until worker shutdown, which logged
       "open socket left in connection". Only reachable with
       cache_turbo_auto_vary on, so the default change above is what exposed
       it.

    *) Adopted the myguard skeleton CI standard: ci/ layout, a single
       pull_request entry point (ci.yml) with every other workflow a
       workflow_call member, the ci/linter/ gate behind one run-all.sh entry
       point, a tracked .githooks/pre-commit, and the badge row and ## CI
       table kept in lockstep by lint-docs-drift.sh.

    *) Fixed ci/tools/lint-shm-lock.sh, which had been reporting "ok" while
       reading ZERO files. Its `cd "$(dirname "$0")/.."` was correct while the
       script lived in tools/ at the repo root and was not updated when the
       tree moved under ci/, so src/*.c matched nothing, the unmatched glob
       stayed a literal, and the `[ -f ]` guard skipped it. The R7 shm-mutex
       invariant is now genuinely checked; it holds in shipped code, so no
       source change was required. An empty selection is exit 2, never 0.

    *) Added lint.yml so the linter gate runs on the PR and not only in the
       commit hook. Both call the same ci/linter/run-all.sh; a CI-only copy
       would drift from the hook and the two would stop agreeing.

    *) Moved Security Scanners beside Fuzzing instead of behind it. The chain
       was sized when scanners took 117s; at 302s it made that lane 970s
       against a 761s budget and the suite's critical path.

    *) Cut the commit hook from 7.4s to 1.47s by no longer running flawfinder,
       cppcheck and semgrep twice per staged commit -- each was both a
       .pre-commit-config.yaml hook and a ci/linter/lint-c.sh scanner, at
       identical thresholds. No gate was dropped: LINT_MODE=all still runs all
       three, and security-scanners.yml owns them on the PR.

    *) Vendored the ast-grep ruleset per module (ci/ast-grep/), so the hook
       gates identically in a standalone clone. ast-grep resolves sgconfig.yml
       by walking UP from the working directory, which outside the superrepo
       finds nothing and scans an empty ruleset while reporting green.

    *) Added gitleaks to ci/linter/install-linters.sh, pinned and
       digest-verified. It gates the staged secret scan and had been present
       only by hand on one build host -- absent from a fresh clone, and absent
       from --check, so nothing reported it missing.

    *) Fixed a Test::Nginx block that asserted only 54 of its 56 requests
       (ci/t/presets/wikijs.t): the --- response_headers array was two entries
       shorter than --- request, and Test::Nginx pairs them positionally, so
       the last pair carried no X-Cache expectation at all.

    *) Gave ci/t/presets/redmine.t TEST 5 and woocommerce.t TEST 6 their own
       paths. cache_turbo_key is $uri, so each shared a cache entry with the
       bypass block above it and its MISS-then-HIT held only because that
       block stores nothing -- an assertion resting on block order.
