Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
65ad945
Route refresh invalidation through unified purge core
yaoge123 Mar 14, 2026
e944b85
Remove refresh wildcard chunk-size dependency
yaoge123 Mar 15, 2026
0812c11
Stabilize refresh scanning across production batches
yaoge123 Mar 15, 2026
d736d28
Harden refresh race safety and defensive checks
yaoge123 Mar 18, 2026
6ad742f
Fix concurrent refresh SIGSEGV caused by stale cache struct reference
yaoge123 Mar 19, 2026
eea295b
Harden code quality and add refresh documentation
yaoge123 Mar 19, 2026
eedbedb
Use background subrequests to eliminate r->main->count overflow
yaoge123 Mar 20, 2026
4001f2b
Restore true HEAD method for refresh subrequests
yaoge123 Mar 21, 2026
6200b4f
Document HEAD method restoration and background subrequest scalability
yaoge123 Mar 21, 2026
8a5911e
Increase default refresh_timeout from 30s to 300s
yaoge123 Mar 21, 2026
b92afdd
Split refresh into a dedicated proxy_cache_refresh directive
yaoge123 Mar 21, 2026
ee7999d
Clarify refresh configuration conflicts and docs
yaoge123 Mar 21, 2026
ff58e5c
Route purge and refresh by request method
yaoge123 Mar 22, 2026
d1b6f24
Handle missing upstream objects during refresh
yaoge123 Mar 22, 2026
3d8a477
Document refresh upstream status policy and summary logging
yaoge123 Mar 22, 2026
9000991
Sync Chinese README with refresh status policy
yaoge123 Mar 22, 2026
3318c6b
Raise refresh summary log level and clarify cache-key docs
yaoge123 Mar 22, 2026
dc2023e
Track refresh status codes separately from errors
yaoge123 Mar 22, 2026
88e3204
Fix active count leak, status-0 accounting, timeout double-count, and…
yaoge123 Mar 22, 2026
2672d42
Allocate temp request on pool and extract common invalidate helper
yaoge123 Mar 22, 2026
1f4a840
Use nginx time syntax for refresh timeout
yaoge123 Mar 23, 2026
debf498
Add byte totals to refresh accounting
yaoge123 Mar 23, 2026
afe26cb
Allow safe prefixed cache keys in refresh
yaoge123 Mar 25, 2026
3cfea73
Use target URIs for safe prefixed refresh keys
yaoge123 Mar 25, 2026
e72b164
tighten refresh cache key tail checks
yaoge123 Mar 28, 2026
4f5b87b
reject partial refresh queries and fix key docs
yaoge123 Mar 30, 2026
e721928
fix refresh fd buildup during large invalidations
yaoge123 Apr 5, 2026
2e75187
Guard refresh capture tails against invalid request captures
yaoge123 Apr 10, 2026
00b48bc
Relax scanned cache node replacement checks
yaoge123 Apr 18, 2026
81eaaa6
Fix refresh transport failure accounting
yaoge123 Apr 18, 2026
af25548
Ignore local worktree directories
yaoge123 Apr 18, 2026
bc21f48
Fix replay merge_loc_conf build breakage
yaoge123 Apr 23, 2026
ec2e133
Fix runtime refresh purge entry regressions
yaoge123 Apr 23, 2026
f2f8cc7
Ignore local test cache artifacts in Docker builds
yaoge123 Apr 23, 2026
56e7be6
docs: align README files with current release line
yaoge123 Apr 24, 2026
3ffb561
test: update compatibility matrix in t/Makefile
yaoge123 Apr 24, 2026
f20893a
test: cover refresh review feedback
yaoge123 Apr 25, 2026
e831d60
fix cache_purge_throttle_ms unit parsing
denji Apr 24, 2026
3d68e64
fix response_type context and duplicate detection
denji Apr 25, 2026
1a64d41
Merge upstream master into replay
yaoge123 Apr 25, 2026
2b27ca3
refactor: split refresh execution into dedicated file
yaoge123 Apr 26, 2026
c2b41de
ci: suppress non-actionable cppcheck info
yaoge123 May 1, 2026
177fe0a
test: add partial refresh coverage for query-aware cache keys
yaoge123 May 4, 2026
1e3b9ec
Merge upstream master (3.0.2 + post-3.0.2 fixes) into replay
yaoge123 May 22, 2026
526f035
Merge upstream master (post-3.0.2 if-block fix rework + TEST 16-20 + …
yaoge123 May 24, 2026
a750464
test: bump compatibility matrix to nginx 1.30.2 stable + 1.31.1 mainline
yaoge123 May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
cache/
cache2/
cache16a/
cache16c/
cache21/
servroot/
perfcache/
bgcache/
concurrent/
throttle/
t/servroot/
t/cache/
t/cache2/
t/cache16a/
t/cache16c/
t/cache21/
t/perfcache/
t/bgcache/
t/concurrent/
t/throttle/
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ jobs:
sudo apt-get update && sudo apt-get install -y cppcheck
cppcheck --enable=all --inconclusive --std=c89 \
--suppress=missingIncludeSystem \
--suppress=toomanyconfigs \
--suppress=checkersReport \
--error-exitcode=1 \
ngx_cache_purge_module.c

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.worktrees/
347 changes: 345 additions & 2 deletions README.md

Large diffs are not rendered by default.

276 changes: 276 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# ngx_cache_purge / refresh 部署说明

这份文档面向部署者,重点说明当前实现里 `purge` / `refresh` 的入口规则、动作路由、能力边界、推荐配置和常见误配。

## 核心规则

- 同一个 `location` 只能配置一个入口指令:`proxy_cache_purge` 或 `proxy_cache_refresh`。
- 对于精确匹配和通配前缀匹配(exact / partial),`proxy_cache_purge` 与 `proxy_cache_refresh` 都同时接受 `PURGE` 和 `REFRESH`。
- exact / partial 最终执行的是 `purge` 还是 `refresh`,由实际 HTTP method 决定,不由 directive 名称决定。
- 成功响应会返回 `X-Cache-Action: purge` 或 `X-Cache-Action: refresh`,用于标识本次实际动作。
- full-zone 请求不能自由互串:`PURGE` 需要 `purge_all`,`REFRESH` 需要 `refresh_all`。
- full-zone 能力不匹配时返回 `400 Bad Request`。
- 配置期规则同样严格:`purge_all` 只能用于 `proxy_cache_purge`,`refresh_all` 只能用于 `proxy_cache_refresh`。
- `proxy_cache_refresh ... purge_all ...` 是非法配置,nginx 在加载配置时就会拒绝。
- refresh 只支持 `proxy_cache`,不支持 `fastcgi_cache`、`scgi_cache`、`uwsgi_cache`。
- 参与 refresh 请求链路的 proxy 配置必须包含:

proxy_cache_bypass $cache_purge_refresh_bypass;
proxy_no_cache $cache_purge_refresh_bypass;

- refresh 能否正确工作,关键在于它能不能从“实际展开后的 cache key”里明确找出请求路径那一段。最稳妥的写法,是让 key 本身就是请求路径(例如 `$uri$is_args$args`、`$request_uri`),或者至少让 key 的最后一段仍然清楚地保留完整的 URI / request URI(例如 `$host$request_uri`、`$scheme$host$request_uri`、`$host$uri$is_args$args`),或者干脆就是精确字面路径(例如 `proxy_cache_key /dir01/file1.txt;`)。这条规则对 separate-location partial refresh 也一样成立:只要最终 key 的尾部仍然是可识别的请求目标,就可以安全工作。如果 refresh 不能可靠判断这一段,它现在会直接返回 `400 Bad Request`,而不是继续猜测并误删缓存。
- partial refresh 如果再带 query 参数(例如 `REFRESH /refresh/path*?v=1`),当前实现也会直接返回 `400 Bad Request`。因为这时 `*` 会落在 `?args` 前面,最终请求目标已经不能再安全映射回扫描前缀。
- 现在 batch / refresh invalidate 在单个对象处理完成后会立即销毁该对象的临时 cache-open pool,不再把这批 pool 一直拖到整个大请求结束再统一释放。
- 这样可以避免大规模 refresh / batch invalidate 时 worker 文件描述符长时间堆积,同时不破坏并发 refresh 的稳定性。

## 动作路由说明

对 exact / partial 请求,动作路由遵循下面这张表:

| 配置的入口指令 | 请求 method | 实际动作 |
| --- | --- | --- |
| `proxy_cache_purge` | `PURGE` | purge |
| `proxy_cache_purge` | `REFRESH` | refresh |
| `proxy_cache_refresh` | `REFRESH` | refresh |
| `proxy_cache_refresh` | `PURGE` | purge |

要点只有一条:

> 对 exact / partial,请把 directive 名称理解成“入口类型”,不要把它误解成“最终动作已经固定”。真正决定动作的是 HTTP method。

但 full-zone 不适用这张自由互串表。full-zone 需要同时满足 method 与 capability 的匹配关系:

| full-zone 请求 | location 必须具备的能力 | 不匹配时结果 |
| --- | --- | --- |
| `PURGE /.../*` | `proxy_cache_purge ... purge_all ...` | `400 Bad Request` |
| `REFRESH /.../*` | `proxy_cache_refresh ... refresh_all ...` | `400 Bad Request` |

## 推荐配置

### 模板一:生产推荐,`/purge` 与 `/refresh` 分离

这是最稳妥的部署方式。运维、权限、监控、限流和回滚都更直观。

```nginx
http {
proxy_cache_path /var/cache/nginx keys_zone=app_cache:100m;

server {
location / {
proxy_pass http://127.0.0.1:8000;
proxy_cache app_cache;
proxy_cache_key "$scheme$host$request_uri";
proxy_cache_bypass $cache_purge_refresh_bypass;
proxy_no_cache $cache_purge_refresh_bypass;
}

location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge app_cache $scheme$host$1$is_args$args;
}

location ~ /refresh(/.*) {
allow 127.0.0.1;
deny all;

proxy_pass http://127.0.0.1:8000;
proxy_cache_bypass $cache_purge_refresh_bypass;
proxy_no_cache $cache_purge_refresh_bypass;

proxy_cache_refresh app_cache $scheme$host$1$is_args$args;
cache_purge_refresh_timeout 600s;
cache_purge_refresh_concurrency 32;
}
}
}
```

适用场景:

- 新部署。
- 希望把 purge 和 refresh 分开做权限控制。
- 希望接口命名与实际运维动作一一对应。

### 模板二:渐进迁移,单入口先切 method

如果你已有 `proxy_cache_purge` 入口,不想马上新增 `/refresh` endpoint,可以先让客户端切换 method。该方案只适合 exact / partial 的过渡期。

```nginx
http {
proxy_cache_path /var/cache/nginx keys_zone=app_cache:100m;

server {
location / {
proxy_pass http://127.0.0.1:8000;
proxy_cache app_cache;
proxy_cache_key "$host$request_uri";
proxy_cache_bypass $cache_purge_refresh_bypass;
proxy_no_cache $cache_purge_refresh_bypass;
proxy_cache_purge PURGE from 127.0.0.1;
}
}
}
```

在这套配置里:

- `PURGE /path/file` -> purge
- `REFRESH /path/file` -> refresh
- `PURGE /dir/*` -> partial purge
- `REFRESH /dir/*` -> partial refresh

注意:单入口 method 兼容不等于 full-zone 也能兼容。`REFRESH /*` 仍然需要一个显式配置了 `proxy_cache_refresh ... refresh_all ...` 的 location。

还要特别注意:单入口渐进迁移只解决“入口不想立刻拆分”的问题,不会自动豁免 refresh 的基础前提。如果请求最终按 `REFRESH` 路由执行,那么参与 refresh 请求链路的 proxy 配置仍然必须满足 bypass/no_cache 约束,cache key 也仍然必须让 refresh 能可靠识别出 URI / request URI 这一段。

`cache_purge_refresh_timeout` 也只是 refresh 的总体软截止时间:超时后会停止继续派发新的校验子请求,但已经在途的子请求会自然收尾,而不是被粗暴中断。

## 能力边界

### `purge_all` 与 `refresh_all` 的边界

- `purge_all` 表示 full-zone purge 能力。
- `refresh_all` 表示 full-zone refresh 能力。
- 二者不能靠切换 method 互相替代。
- 运行时 capability mismatch 返回 `400 Bad Request`。

### refresh 的实现边界

- refresh 只支持 `proxy_cache`。
- refresh 会向 upstream 发带条件头(`If-None-Match` / `If-Modified-Since`)的校验子请求,并读取缓存文件里的 `ETag` / `Last-Modified`。
- nginx 内部仍可能把上游方法转成 `GET`,但 refresh 路径会强制只处理响应头、不读取响应体;真正的带宽收益主要来自 `304 Not Modified` 和不读 body。
- upstream 返回 `304` 时保留缓存;返回 `200` 时走正常 invalidate 路径;返回 `404` / `410` 时直接 purge;其它 HTTP 状态默认保留缓存,并额外记入状态码统计;只有内部/传输失败才计入 `errors`。

### cache key 边界

可工作的典型 key:

- `$uri`
- `$uri$is_args$args`
- `$host$uri$is_args$args`
- `$host$request_uri`
- `$scheme$host$request_uri`
- 精确字面路径,例如 `proxy_cache_key /dir01/file1.txt;`

不可靠的典型 key:

- `$uri$host`
- `$request_uri$cookie_user`
- `$request_uri$host`
- `$host$request_uri$arg_v`
- `$arg_x$uri$host`
- partial refresh 请求里再拼 `?args`,例如 `REFRESH /refresh/path*?v=1`

判断标准不是“key 里有没有出现 URI”,而是“refresh 能不能从最终 key 里明确识别出请求路径的最后一段”。像 `$arg_x$uri$host`、`$uri|suffix` 这种写法虽然也包含 URI,但 URI 只出现在中间,或者尾部又拼了别的维度,refresh 就无法安全反推出上游请求。遇到这种情况,当前实现会直接拒绝 refresh(`400 Bad Request`),而不是冒险继续执行。

## 常见错误配置

### 错误一:同一个 `location` 里同时放两个入口指令

```nginx
location /control/ {
proxy_cache_purge PURGE from 127.0.0.1;
proxy_cache_refresh REFRESH from 127.0.0.1;
}
```

为什么错:同一个 `location` 只能有一个入口指令。这个组合会在配置期被拒绝。

### 错误二:把 `purge_all` 写到 `proxy_cache_refresh` 上

```nginx
location /refresh-all/ {
proxy_cache_refresh REFRESH purge_all from 127.0.0.1;
}
```

为什么错:`purge_all` 属于 `proxy_cache_purge`,这里必须写 `refresh_all`。

### 错误三:把 full-zone `PURGE` 发到只有 `refresh_all` 的入口

```nginx
location /refresh-all/ {
allow 127.0.0.1;
deny all;
proxy_pass http://127.0.0.1:8000;
proxy_cache_bypass $cache_purge_refresh_bypass;
proxy_no_cache $cache_purge_refresh_bypass;
proxy_cache_refresh REFRESH refresh_all from 127.0.0.1;
}
```

如果这里是 `REFRESH ... refresh_all ...` 能力,那么客户端发 full-zone `PURGE` 仍会因为 capability mismatch 返回 `400 Bad Request`。

### 错误四:refresh 链路缺少 bypass / no_cache

```nginx
location / {
proxy_pass http://127.0.0.1:8000;
proxy_cache app_cache;
proxy_cache_key "$host$request_uri";
}
```

为什么错:如果 refresh 子请求最终走到这样的 proxy location,而没有:

```nginx
proxy_cache_bypass $cache_purge_refresh_bypass;
proxy_no_cache $cache_purge_refresh_bypass;
```

那么 refresh 可能误读本地缓存,或者把校验请求重新写回缓存。

### 错误五:把 refresh 用到非 proxy cache

```nginx
location /fcgi-refresh/ {
fastcgi_cache_purge REFRESH from 127.0.0.1;
}
```

为什么错:当前 refresh 不支持 `fastcgi` / `scgi` / `uwsgi`。

### 错误六:cache key 的末尾不是 URI / request URI

```nginx
location / {
proxy_pass http://127.0.0.1:8000;
proxy_cache app_cache;
proxy_cache_key "$arg_x$uri$host";
proxy_cache_purge PURGE from 127.0.0.1;
}
```

为什么错:refresh 需要从缓存 key 的最后一段反推出上游请求路径。只要 URI / request URI 不在 key 的末尾,这个反推过程就不可靠。

## 生产部署建议

- 新部署优先使用“双入口”模型:`/purge/...` 和 `/refresh/...` 分离。
- 只有在迁移期才建议使用“单入口 + method 切换”。
- refresh endpoint 建议做单独的访问控制、限流、审计和监控。
- 对大规模 refresh,结合业务体量调 `cache_purge_refresh_concurrency` 和 `cache_purge_refresh_timeout`。
- 生产前先核对 `proxy_cache_key` 是否以 URI / request URI 结尾;这是功能正确性的前提,不只是文档建议。
- 检查 refresh 子请求实际会经过哪些 proxy location,确保这些 location 都正确配置了 `proxy_cache_bypass` 与 `proxy_no_cache`。
- 把 `X-Cache-Action` 纳入日志或审计字段,这样能快速确认一次请求最终执行的是 purge 还是 refresh。

## 兼容与测试

- 当前兼容矩阵遵循这条规则:覆盖 latest stable、latest mainline,以及从 `1.20.x` 到当前 stable 之间的所有 legacy stable lines。
- 当前具体回归版本为:`1.20.2`、`1.22.1`、`1.24.0`、`1.26.3`、`1.28.3`、latest stable、latest mainline。
- 每次跑矩阵前,都要先检查 nginx 官方下载页;如果 latest stable、latest mainline 或任一 legacy stable line 已更新,就先刷新具体版本号再测试。
- 在 `t/` 目录下优先使用 Docker 化入口:`make test`、`make test-all`、`make test-version VERSION=<x.y.z>`、`make test-compat`。
- repo 级集成回归回退命令仍然是仓库根目录下的 `./scripts/run_regression.sh`。

## 响应与排错提示

- refresh 成功时返回 `200 OK`;refresh 目前只支持 JSON 或 text 两种正文格式:当 `cache_purge_response_type=json` 时返回 JSON,例如 `{"status":"refresh",...,"total_bytes":12345,"kept_bytes":6789,"purged_bytes":5556,"status_counts":{"200":190,"301":1,"304":15375},"status_bytes":{"200":1048576,"301":512,"304":2097152}}`;其余取值都会回退到 text。默认 text 模式下,正文类似 `Refresh: total=<N> kept=<K> purged=<P> errors=<E> total_bytes=<B> kept_bytes=<KB> purged_bytes=<PB> statuses={200:<N>,304:<N>,...} status_bytes={200:<BYTES>,304:<BYTES>,...}`。
- 其中统计口径是:`kept` 表示本次 refresh 的最终动作是保留缓存(例如上游返回 `304`、`301`、`403`、`500`,或者竞态下保留缓存);`purged` 表示最终动作是清理缓存(当前主要对应上游返回 `200`、`404`、`410` 且 invalidate 成功);`errors` 只表示真正需要排查的失败,例如子请求创建失败、超时、传输失败、内部 invalidate/helper 失败。
- `total_bytes` 表示本次匹配到的缓存条目总大小;`kept_bytes` / `purged_bytes` 分别表示最终保留 / 清理的缓存条目大小总和。
- `statuses={...}` 表示本次 refresh 实际观察到的 upstream HTTP 状态码统计;`status_bytes={...}` 表示这些状态码对应条目的缓存大小总和。两者都只显示本次请求里真正出现过的状态码,不做穷举。
- `*_bytes` 的统计基于 refresh 扫描阶段看到的缓存文件大小(`fs_size`)。如果 refresh 过程中发生并发 race,这些字节数属于近似值,不保证和最终磁盘状态完全一致。
- purge / refresh 成功时都带 `X-Cache-Action` 响应头。
- full-zone 能力错配时返回 `400 Bad Request`,优先检查是不是把 `PURGE` 发到了 `refresh_all` location,或者把 `REFRESH` 发到了 `purge_all` location。
- refresh 当前对上游状态码的策略是保守的:`304` 保留,`200` 走正常 invalidate 路径,`404` / `410` 直接 purge,其它 HTTP 状态默认保留并写入 `statuses={...}`;只有内部/传输失败才计入 `errors`。
- 每次 refresh 结束时,模块还会在 `error_log notice` 写一条汇总日志,例如 `cache refresh summary uri="/path/*" total=<N> kept=<K> purged=<P> errors=<E> timed_out=<0|1> total_bytes=<B> kept_bytes=<KB> purged_bytes=<PB> statuses={200:190,301:1,304:15375} status_bytes={200:1048576,301:512,304:2097152}`;逐条条目的明细仍然只在 debug 日志中可见。
- 如果 refresh 结果异常,优先检查三件事:是否是 `proxy_cache`、refresh 链路上是否配置 bypass/no_cache、cache key 是否以 URI/request URI 结尾。
2 changes: 1 addition & 1 deletion config
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fi
# Module identity and sources
# ------------------------------------------------------------------
ngx_addon_name=ngx_http_cache_purge_module
CACHE_PURGE_SRCS="$ngx_addon_dir/ngx_cache_purge_module.c"
CACHE_PURGE_SRCS="$ngx_addon_dir/ngx_cache_purge_module.c $ngx_addon_dir/ngx_http_cache_refresh.c"

# ------------------------------------------------------------------
# Register the module with nginx's build system.
Expand Down
Loading
Loading