From 65ad9454b18f0b7e430fd9b4f74afcc6791aca4a Mon Sep 17 00:00:00 2001 From: yaoge123 Date: Sun, 15 Mar 2026 07:40:19 +0800 Subject: [PATCH 01/43] Route refresh invalidation through unified purge core --- ngx_cache_purge_module.c | 1451 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 1393 insertions(+), 58 deletions(-) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index 6c6c7c2..efa41fe 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -151,11 +151,12 @@ struct ngx_http_cache_purge_main_conf_s { }; typedef struct { - ngx_flag_t enable; - ngx_str_t method; - ngx_flag_t purge_all; - ngx_array_t *access; /* ngx_in_cidr_t */ - ngx_array_t *access6; /* ngx_in6_cidr_t */ + ngx_flag_t enable; + ngx_str_t method; + ngx_flag_t purge_all; + ngx_flag_t refresh; + ngx_array_t *access; /* array of ngx_in_cidr_t */ + ngx_array_t *access6; /* array of ngx_in6_cidr_t */ } ngx_http_cache_purge_conf_t; typedef struct { @@ -188,6 +189,8 @@ typedef struct { ngx_http_complex_value_t *proxy_separate_value; /* dynamic zone expr */ ngx_http_complex_value_t proxy_separate_key; /* purge key template*/ # endif + ngx_uint_t refresh_concurrency; + ngx_msec_t refresh_timeout; } ngx_http_cache_purge_loc_conf_t; typedef struct { @@ -271,6 +274,10 @@ static ngx_int_t ngx_http_purge_file_cache_delete_partial_file( */ static ngx_int_t ngx_http_purge_file_cache_delete_exact_file( ngx_tree_ctx_t *ctx, ngx_str_t *path); +static ngx_int_t ngx_http_cache_purge_invalidate_file(ngx_tree_ctx_t *ctx, + ngx_str_t *path); +static ngx_int_t ngx_http_cache_purge_invalidate_partial_file( + ngx_tree_ctx_t *ctx, ngx_str_t *path); static void ngx_http_cache_purge_delete_variants(ngx_http_request_t *r, ngx_http_file_cache_t *cache); @@ -300,6 +307,117 @@ void *ngx_http_cache_purge_create_loc_conf(ngx_conf_t *cf); char *ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); +typedef enum { + NGX_HTTP_CACHE_PURGE_INVALIDATE_PURGED = 0, + NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING, + NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_REPLACED, + NGX_HTTP_CACHE_PURGE_INVALIDATE_ERROR +} ngx_http_cache_purge_invalidate_result_e; + +typedef struct { + ngx_str_t cache_key; + ngx_str_t cache_path; + time_t last_modified; + u_char etag_len; + u_char etag[NGX_HTTP_CACHE_ETAG_LEN]; + off_t fs_size; +} ngx_http_cache_purge_invalidate_item_t; + +typedef struct { + ngx_http_request_t *request; + ngx_http_file_cache_t *cache; + ngx_str_t partial_prefix; + ngx_flag_t match_all; + ngx_uint_t files_deleted; +} ngx_http_cache_purge_batch_ctx_t; + +static ngx_int_t ngx_http_cache_purge_read_item(ngx_pool_t *pool, + ngx_log_t *log, ngx_str_t *path, + ngx_http_cache_purge_invalidate_item_t *item); +static ngx_int_t ngx_http_cache_purge_invalidate_opened_cache(ngx_log_t *log, + ngx_http_file_cache_t *cache, ngx_http_cache_t *c, + ngx_pool_t *pool, ngx_http_cache_purge_invalidate_item_t *item, + ngx_http_cache_purge_invalidate_result_e *result); +static ngx_int_t ngx_http_cache_purge_open_temp_cache(ngx_http_request_t *r, + ngx_http_file_cache_t *cache, ngx_pool_t *pool, ngx_str_t *cache_key, + ngx_http_cache_t *c); +static ngx_int_t ngx_http_cache_purge_item_metadata_matches( + ngx_http_cache_purge_invalidate_item_t *expected, + ngx_http_cache_purge_invalidate_item_t *current); +static ngx_int_t ngx_http_cache_purge_invalidate_item(ngx_http_request_t *r, + ngx_http_file_cache_t *cache, ngx_pool_t *pool, + ngx_http_cache_purge_invalidate_item_t *item, + ngx_http_cache_purge_invalidate_result_e *result); + +typedef struct { + ngx_str_t uri; + ngx_str_t args; + ngx_str_t etag; + time_t last_modified; + ngx_str_t path; + ngx_http_cache_purge_invalidate_item_t item; +} ngx_http_cache_purge_refresh_file_t; + +typedef struct { + ngx_http_request_t *request; + ngx_http_file_cache_t *cache; + ngx_str_t key_partial; + ngx_uint_t key_prefix_len; + ngx_flag_t purge_all; + ngx_flag_t exact; + ngx_flag_t timed_out; + ngx_flag_t finalized; + ngx_msec_t deadline; + ngx_event_t timeout_ev; + ngx_pool_t *chunk_pool; + ngx_array_t *files; + ngx_uint_t current; + ngx_uint_t queued; + ngx_uint_t active; + ngx_uint_t chunk_limit; + ngx_flag_t scan_done; + ngx_str_t scan_after; + ngx_str_t resume_path; + ngx_uint_t total; + ngx_uint_t refreshed; + ngx_uint_t purged; + ngx_uint_t errors; +} ngx_http_cache_purge_refresh_ctx_t; + +typedef struct { + ngx_http_cache_purge_refresh_ctx_t *ctx; + ngx_http_cache_purge_refresh_file_t *file; +} ngx_http_cache_purge_refresh_post_data_t; + +static ngx_int_t ngx_http_cache_purge_refresh(ngx_http_request_t *r, + ngx_http_file_cache_t *cache); +static ngx_int_t ngx_http_cache_purge_refresh_collect_file( + ngx_tree_ctx_t *ctx, ngx_str_t *path); +static ngx_int_t ngx_http_cache_purge_refresh_collect_open_file( + ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); +static ngx_int_t ngx_http_cache_purge_refresh_collect_path( + ngx_http_cache_purge_refresh_ctx_t *rctx, ngx_str_t *path, + ngx_uint_t exact_match); +static void ngx_http_cache_purge_refresh_start(ngx_http_request_t *r); +static ngx_int_t ngx_http_cache_purge_refresh_fire_subrequest( + ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); +static ngx_int_t ngx_http_cache_purge_refresh_done( + ngx_http_request_t *r, void *data, ngx_int_t rc); +static ngx_int_t ngx_http_cache_purge_refresh_send_response( + ngx_http_request_t *r); +static void ngx_http_cache_purge_refresh_timeout_handler(ngx_event_t *ev); +static void ngx_http_cache_purge_refresh_mark_timeout( + ngx_http_cache_purge_refresh_ctx_t *ctx); +static void ngx_http_cache_purge_refresh_finalize( + ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); +static ngx_int_t ngx_http_cache_purge_refresh_scan_next_chunk( + ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); +static ngx_int_t ngx_http_cache_purge_refresh_path_cmp( + ngx_str_t *a, ngx_str_t *b); +static ngx_int_t ngx_http_cache_purge_add_variable(ngx_conf_t *cf); +static ngx_int_t ngx_http_cache_purge_refresh_bypass_variable( + ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); + /* -- module commands ---------------------------------------------------- */ @@ -374,6 +492,20 @@ static ngx_command_t ngx_http_cache_purge_module_commands[] = { NGX_HTTP_MAIN_CONF_OFFSET, offsetof(ngx_http_cache_purge_main_conf_t, vary_aware), NULL }, + { ngx_string("cache_purge_refresh_concurrency"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_cache_purge_loc_conf_t, refresh_concurrency), + NULL }, + + { ngx_string("cache_purge_refresh_timeout"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_cache_purge_loc_conf_t, refresh_timeout), + NULL }, + ngx_null_command }; @@ -381,7 +513,7 @@ static ngx_command_t ngx_http_cache_purge_module_commands[] = { /* -- module context & descriptor ---------------------------------------- */ static ngx_http_module_t ngx_http_cache_purge_module_ctx = { - NULL, /* preconfiguration */ + ngx_http_cache_purge_add_variable, /* preconfiguration */ NULL, /* postconfiguration */ ngx_http_cache_purge_create_main_conf, /* create main conf */ ngx_http_cache_purge_init_main_conf, /* init main conf */ @@ -1254,7 +1386,17 @@ ngx_http_fastcgi_cache_purge_conf(ngx_conf_t *cf, ngx_command_t *cmd, } if (cf->args->nelts != 3) { - return ngx_http_cache_purge_conf(cf, &cplcf->fastcgi); + if (ngx_http_cache_purge_conf(cf, &cplcf->fastcgi) != NGX_CONF_OK) { + return NGX_CONF_ERROR; + } + + if (cplcf->fastcgi.refresh) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"refresh\" is supported only with \"proxy_cache_purge\""); + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; } if (cf->cmd_type & (NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF)) { @@ -1771,6 +1913,10 @@ ngx_http_proxy_cache_purge_handler(ngx_http_request_t *r) cmcf = ngx_http_get_module_main_conf(r, ngx_http_cache_purge_module); cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); + if (cplcf->conf->refresh) { + return ngx_http_cache_purge_refresh(r, cache); + } + if (cmcf != NULL && cmcf->background_purge && (cplcf->conf->purge_all || ngx_http_cache_purge_is_partial(r))) { @@ -1881,7 +2027,17 @@ ngx_http_scgi_cache_purge_conf(ngx_conf_t *cf, ngx_command_t *cmd, } if (cf->args->nelts != 3) { - return ngx_http_cache_purge_conf(cf, &cplcf->scgi); + if (ngx_http_cache_purge_conf(cf, &cplcf->scgi) != NGX_CONF_OK) { + return NGX_CONF_ERROR; + } + + if (cplcf->scgi.refresh) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"refresh\" is supported only with \"proxy_cache_purge\""); + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; } if (cf->cmd_type & (NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF)) { @@ -2133,7 +2289,17 @@ ngx_http_uwsgi_cache_purge_conf(ngx_conf_t *cf, ngx_command_t *cmd, } if (cf->args->nelts != 3) { - return ngx_http_cache_purge_conf(cf, &cplcf->uwsgi); + if (ngx_http_cache_purge_conf(cf, &cplcf->uwsgi) != NGX_CONF_OK) { + return NGX_CONF_ERROR; + } + + if (cplcf->uwsgi.refresh) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"refresh\" is supported only with \"proxy_cache_purge\""); + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; } if (cf->cmd_type & (NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF)) { @@ -2345,6 +2511,254 @@ ngx_http_cache_purge_response_type_conf(ngx_conf_t *cf, ngx_command_t *cmd, /* -- access control ----------------------------------------------------- */ +static ngx_int_t +ngx_http_cache_purge_invalidate_file(ngx_tree_ctx_t *ctx, ngx_str_t *path) +{ + ngx_http_cache_purge_batch_ctx_t *data; + ngx_http_cache_purge_invalidate_item_t item; + ngx_http_cache_purge_invalidate_result_e result; + ngx_pool_t *pool; + + data = ctx->data; + pool = ngx_create_pool(4096, ctx->log); + if (pool == NULL) { + return NGX_OK; + } + + if (ngx_http_cache_purge_read_item(pool, ctx->log, path, &item) == NGX_OK) { + if (ngx_http_cache_purge_invalidate_item(data->request, data->cache, + pool, &item, &result) + != NGX_OK) + { + ngx_log_error(NGX_LOG_CRIT, ctx->log, 0, + "http file cache invalidate failed for \"%V\"", + path); + } else if (result == NGX_HTTP_CACHE_PURGE_INVALIDATE_PURGED) { + data->files_deleted++; + } + } + + ngx_destroy_pool(pool); + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_cache_purge_invalidate_partial_file(ngx_tree_ctx_t *ctx, + ngx_str_t *path) +{ + ngx_http_cache_purge_batch_ctx_t *data; + ngx_http_cache_purge_invalidate_item_t item; + ngx_http_cache_purge_invalidate_result_e result; + ngx_pool_t *pool; + + data = ctx->data; + pool = ngx_create_pool(4096, ctx->log); + if (pool == NULL) { + return NGX_OK; + } + + if (ngx_http_cache_purge_read_item(pool, ctx->log, path, &item) == NGX_OK) { + if (data->partial_prefix.len == 0 + || (item.cache_key.len >= data->partial_prefix.len + && ngx_strncasecmp(item.cache_key.data, + data->partial_prefix.data, + data->partial_prefix.len) == 0)) + { + if (ngx_http_cache_purge_invalidate_item(data->request, data->cache, + pool, &item, &result) + != NGX_OK) + { + ngx_log_error(NGX_LOG_CRIT, ctx->log, 0, + "http partial cache invalidate failed for \"%V\"", + path); + } else if (result == NGX_HTTP_CACHE_PURGE_INVALIDATE_PURGED) { + data->files_deleted++; + } + } + } + + ngx_destroy_pool(pool); + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_cache_purge_invalidate_opened_cache(ngx_log_t *log, + ngx_http_file_cache_t *cache, ngx_http_cache_t *c, + ngx_pool_t *pool, ngx_http_cache_purge_invalidate_item_t *item, + ngx_http_cache_purge_invalidate_result_e *result) +{ + ngx_http_cache_purge_invalidate_item_t current_item; + + ngx_shmtx_lock(&cache->shpool->mutex); + + if (!c->node->exists) { + ngx_shmtx_unlock(&cache->shpool->mutex); + *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING; + return NGX_OK; + } + + if (item != NULL) { + if (ngx_http_cache_purge_read_item(pool, log, &c->file.name, + ¤t_item) + != NGX_OK) + { + ngx_shmtx_unlock(&cache->shpool->mutex); + *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING; + return NGX_OK; + } + + if (!ngx_http_cache_purge_item_metadata_matches(item, ¤t_item)) { + ngx_shmtx_unlock(&cache->shpool->mutex); + *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_REPLACED; + return NGX_OK; + } + } + +# if (nginx_version >= 1000001) + cache->sh->size -= c->node->fs_size; + c->node->fs_size = 0; +# else + cache->sh->size -= (c->node->length + cache->bsize - 1) / cache->bsize; + c->node->length = 0; +# endif + + c->node->exists = 0; +# if (nginx_version >= 8001) \ + || ((nginx_version < 8000) && (nginx_version >= 7060)) + c->node->updating = 0; +# endif + + ngx_shmtx_unlock(&cache->shpool->mutex); + + if (ngx_delete_file(c->file.name.data) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_CRIT, log, ngx_errno, + ngx_delete_file_n " \"%s\" failed", c->file.name.data); + } + + *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_PURGED; + return NGX_OK; +} + + +static ngx_int_t +ngx_http_cache_purge_open_temp_cache(ngx_http_request_t *r, + ngx_http_file_cache_t *cache, ngx_pool_t *pool, ngx_str_t *cache_key, + ngx_http_cache_t *c) +{ + ngx_http_request_t tr; + ngx_str_t *key; + + ngx_memzero(c, sizeof(ngx_http_cache_t)); + ngx_memcpy(&tr, r, sizeof(ngx_http_request_t)); + + tr.pool = pool; + tr.cache = c; + + if (ngx_array_init(&c->keys, pool, 1, sizeof(ngx_str_t)) != NGX_OK) { + return NGX_ERROR; + } + + key = ngx_array_push(&c->keys); + if (key == NULL) { + return NGX_ERROR; + } + + *key = *cache_key; + c->body_start = ngx_pagesize; + c->file_cache = cache; + c->file.log = r->connection->log; + + ngx_http_file_cache_create_key(&tr); + + switch (ngx_http_file_cache_open(&tr)) { + case NGX_OK: + case NGX_HTTP_CACHE_STALE: +# if (nginx_version >= 8001) \ + || ((nginx_version < 8000) && (nginx_version >= 7060)) + case NGX_HTTP_CACHE_UPDATING: +# endif + return NGX_OK; + + case NGX_DECLINED: + return NGX_DECLINED; + + default: + return NGX_ERROR; + } +} + + +static ngx_int_t +ngx_http_cache_purge_item_metadata_matches( + ngx_http_cache_purge_invalidate_item_t *expected, + ngx_http_cache_purge_invalidate_item_t *current) +{ + ngx_uint_t matched = 0; + + if (expected->cache_key.len != current->cache_key.len + || ngx_strncmp(expected->cache_key.data, current->cache_key.data, + expected->cache_key.len) != 0) + { + return 0; + } + + if (expected->etag_len > 0) { + if (expected->etag_len != current->etag_len + || ngx_memcmp(expected->etag, current->etag, expected->etag_len) != 0) + { + return 0; + } + matched = 1; + } + + if (expected->last_modified > 0) { + if (expected->last_modified != current->last_modified) { + return 0; + } + matched = 1; + } + + if (expected->fs_size > 0) { + if (expected->fs_size != current->fs_size) { + return 0; + } + matched = 1; + } + + return matched; +} + + +static ngx_int_t +ngx_http_cache_purge_invalidate_item(ngx_http_request_t *r, + ngx_http_file_cache_t *cache, ngx_pool_t *pool, + ngx_http_cache_purge_invalidate_item_t *item, + ngx_http_cache_purge_invalidate_result_e *result) +{ + ngx_http_cache_t c; + ngx_int_t rc; + + *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_ERROR; + + rc = ngx_http_cache_purge_open_temp_cache(r, cache, pool, &item->cache_key, &c); + if (rc == NGX_DECLINED) { + *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING; + return NGX_OK; + } + + if (rc != NGX_OK) { + return NGX_ERROR; + } + + return ngx_http_cache_purge_invalidate_opened_cache(r->connection->log, + cache, &c, pool, item, + result); +} + ngx_int_t ngx_http_cache_purge_access_handler(ngx_http_request_t *r) { @@ -2711,6 +3125,7 @@ ngx_http_file_cache_purge(ngx_http_request_t *r) { ngx_http_file_cache_t *cache; ngx_http_cache_t *c; + ngx_http_cache_purge_invalidate_result_e result; switch (ngx_http_file_cache_open(r)) { case NGX_OK: @@ -2736,37 +3151,17 @@ ngx_http_file_cache_purge(ngx_http_request_t *r) c = r->cache; cache = c->file_cache; - ngx_shmtx_lock(&cache->shpool->mutex); - - if (!c->node->exists) { - ngx_shmtx_unlock(&cache->shpool->mutex); - return NGX_DECLINED; + if (ngx_http_cache_purge_invalidate_opened_cache(r->connection->log, + cache, c, NULL, NULL, + &result) + != NGX_OK) + { + return NGX_ERROR; } -# if (nginx_version >= 1000001) - cache->sh->size -= c->node->fs_size; - c->node->fs_size = 0; -# else - cache->sh->size -= (c->node->length + cache->bsize - 1) / cache->bsize; - c->node->length = 0; -# endif - - c->node->exists = 0; -# if (nginx_version >= 8001) \ - || ((nginx_version < 8000) && (nginx_version >= 7060)) - c->node->updating = 0; -# endif - - ngx_shmtx_unlock(&cache->shpool->mutex); - - if (ngx_delete_file(c->file.name.data) == NGX_FILE_ERROR) { - ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno, - "ngx_cache_purge: could not delete \"%V\"", &c->file.name); - } else { - ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "ngx_cache_purge: deleted \"%V\"", &c->file.name); + if (result == NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING) { + return NGX_DECLINED; } - return NGX_OK; } @@ -2776,13 +3171,18 @@ ngx_http_file_cache_purge(ngx_http_request_t *r) void ngx_http_cache_purge_all(ngx_http_request_t *r, ngx_http_file_cache_t *cache) { - ngx_http_cache_purge_walk_ctx_t ctx; - ngx_tree_ctx_t tree; + ngx_http_cache_purge_batch_ctx_t ctx; + ngx_tree_ctx_t tree; - ngx_memzero(&ctx, sizeof(ngx_http_cache_purge_walk_ctx_t)); + ngx_memzero(&ctx, sizeof(ngx_http_cache_purge_batch_ctx_t)); ngx_memzero(&tree, sizeof(ngx_tree_ctx_t)); - tree.file_handler = ngx_http_purge_file_cache_delete_file; + ctx.request = r; + ctx.cache = cache; + ctx.match_all = 1; + ctx.files_deleted = 0; + + tree.file_handler = ngx_http_cache_purge_invalidate_file; tree.pre_tree_handler = ngx_http_purge_file_cache_noop; tree.post_tree_handler = ngx_http_purge_file_cache_noop; tree.spec_handler = ngx_http_purge_file_cache_noop; @@ -2790,20 +3190,16 @@ ngx_http_cache_purge_all(ngx_http_request_t *r, ngx_http_file_cache_t *cache) tree.log = ngx_cycle->log; ngx_walk_tree(&tree, &cache->path->name); - - ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "ngx_cache_purge: purge_all deleted %ui file(s) " - "from zone \"%V\"", ctx.files_deleted, &cache->path->name); } ngx_uint_t ngx_http_cache_purge_partial(ngx_http_request_t *r, ngx_http_file_cache_t *cache) { - ngx_http_cache_purge_walk_ctx_t ctx; - ngx_tree_ctx_t tree; - ngx_str_t *key; - ngx_uint_t len; + ngx_http_cache_purge_batch_ctx_t ctx; + ngx_tree_ctx_t tree; + ngx_str_t *key; + ngx_uint_t len; key = r->cache->keys.elts; len = key[0].len; @@ -2812,13 +3208,16 @@ ngx_http_cache_purge_partial(ngx_http_request_t *r, len--; } - ngx_memzero(&ctx, sizeof(ngx_http_cache_purge_walk_ctx_t)); + ngx_memzero(&ctx, sizeof(ngx_http_cache_purge_batch_ctx_t)); ngx_memzero(&tree, sizeof(ngx_tree_ctx_t)); - ctx.key_partial = key[0].data; - ctx.key_len = len; + ctx.request = r; + ctx.cache = cache; + ctx.partial_prefix.data = key[0].data; + ctx.partial_prefix.len = len; + ctx.files_deleted = 0; - tree.file_handler = ngx_http_purge_file_cache_delete_partial_file; + tree.file_handler = ngx_http_cache_purge_invalidate_partial_file; tree.pre_tree_handler = ngx_http_purge_file_cache_noop; tree.post_tree_handler = ngx_http_purge_file_cache_noop; tree.spec_handler = ngx_http_purge_file_cache_noop; @@ -2827,10 +3226,6 @@ ngx_http_cache_purge_partial(ngx_http_request_t *r, ngx_walk_tree(&tree, &cache->path->name); - ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, - "ngx_cache_purge: partial walk deleted %ui file(s) " - "for key prefix \"%V\"", ctx.files_deleted, &key[0]); - return ctx.files_deleted; } @@ -2883,6 +3278,16 @@ ngx_http_cache_purge_conf(ngx_conf_t *cf, ngx_http_cache_purge_conf_t *cpcf) from_position++; } + /* We will refresh (validate) instead of blindly purging */ + if (from_position < cf->args->nelts + && ngx_strcmp(value[from_position].data, "refresh") == 0) + { + cpcf->refresh = 1; + from_position++; + } + + + /* sanity check */ if (ngx_strcmp(value[from_position].data, "from") != 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\", expected \"from\" keyword", @@ -2971,6 +3376,7 @@ ngx_http_cache_purge_merge_conf(ngx_http_cache_purge_conf_t *conf, conf->purge_all = prev->purge_all; conf->access = prev->access; conf->access6 = prev->access6; + conf->refresh = prev->refresh; } else { conf->enable = 0; } @@ -3010,7 +3416,10 @@ ngx_http_cache_purge_create_loc_conf(ngx_conf_t *cf) # endif conf->resptype = NGX_CONF_UNSET_UINT; - conf->conf = NGX_CONF_UNSET_PTR; + conf->refresh_concurrency = NGX_CONF_UNSET_UINT; + conf->refresh_timeout = NGX_CONF_UNSET_MSEC; + + conf->conf = NGX_CONF_UNSET_PTR; return conf; } @@ -3027,6 +3436,11 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_uint_value(conf->resptype, prev->resptype, NGX_REPONSE_TYPE_HTML); + ngx_conf_merge_uint_value(conf->refresh_concurrency, + prev->refresh_concurrency, 32); + ngx_conf_merge_msec_value(conf->refresh_timeout, + prev->refresh_timeout, 30000); + # if (NGX_HTTP_FASTCGI) ngx_http_cache_purge_merge_conf(&conf->fastcgi, &prev->fastcgi); @@ -3095,6 +3509,927 @@ ngx_http_cache_purge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) } +/* + * ===================== Refresh Feature Implementation ===================== + */ + +static ngx_str_t ngx_http_cache_purge_refresh_bypass_name = + ngx_string("cache_purge_refresh_bypass"); + +static ngx_str_t ngx_http_head_method_name = ngx_string("HEAD"); + + +/* + * Variable handler for $cache_purge_refresh_bypass. + * Returns "1" when the current request is a refresh subrequest, + * empty otherwise. Used with proxy_cache_bypass and proxy_no_cache. + */ +static ngx_int_t +ngx_http_cache_purge_refresh_bypass_variable(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data) +{ + ngx_http_cache_purge_refresh_ctx_t *ctx; + + if (r->parent != NULL) { + ctx = ngx_http_get_module_ctx(r->parent, + ngx_http_cache_purge_module); + if (ctx != NULL) { + v->len = 1; + v->valid = 1; + v->no_cacheable = 1; + v->not_found = 0; + v->data = (u_char *) "1"; + return NGX_OK; + } + } + + v->not_found = 1; + return NGX_OK; +} + + +/* + * Register the $cache_purge_refresh_bypass variable during preconfiguration. + */ +static ngx_int_t +ngx_http_cache_purge_add_variable(ngx_conf_t *cf) +{ + ngx_http_variable_t *var; + + var = ngx_http_add_variable(cf, + &ngx_http_cache_purge_refresh_bypass_name, + NGX_HTTP_VAR_NOCACHEABLE); + if (var == NULL) { + return NGX_ERROR; + } + + var->get_handler = ngx_http_cache_purge_refresh_bypass_variable; + + return NGX_OK; +} + + +/* + * Tree walk callback: collect matching cache files for refresh. + * Reads each cache file's binary header to extract ETag and Last-Modified, + * and extracts the URI from the stored cache key. + */ +static ngx_int_t +ngx_http_cache_purge_refresh_collect_file(ngx_tree_ctx_t *ctx, + ngx_str_t *path) +{ + return ngx_http_cache_purge_refresh_collect_path(ctx->data, path, 0); +} + + +static ngx_int_t +ngx_http_cache_purge_refresh_collect_open_file(ngx_http_request_t *r, + ngx_http_cache_purge_refresh_ctx_t *ctx) +{ + switch (ngx_http_file_cache_open(r)) { + case NGX_OK: + case NGX_HTTP_CACHE_STALE: +# if (nginx_version >= 8001) \ + || ((nginx_version < 8000) && (nginx_version >= 7060)) + case NGX_HTTP_CACHE_UPDATING: +# endif + return ngx_http_cache_purge_refresh_collect_path(ctx, + &r->cache->file.name, + 1); + + case NGX_DECLINED: + return NGX_OK; + +# if (NGX_HAVE_FILE_AIO) + case NGX_AGAIN: +# endif + default: + return NGX_ERROR; + } +} + + +static ngx_int_t +ngx_http_cache_purge_refresh_path_cmp(ngx_str_t *a, ngx_str_t *b) +{ + return ngx_memn2cmp(a->data, b->data, a->len, b->len); +} + + +static ngx_int_t +ngx_http_cache_purge_refresh_collect_path( + ngx_http_cache_purge_refresh_ctx_t *rctx, ngx_str_t *path, + ngx_uint_t exact_match) +{ + ngx_http_cache_purge_refresh_file_t *file; + ngx_http_file_cache_header_t header; + ngx_file_t f; + ngx_file_info_t fi; + u_char *key_buf; + ssize_t n; + size_t key_read_len; + u_char *p, *q; + + if (!exact_match && rctx->scan_after.len > 0 + && ngx_http_cache_purge_refresh_path_cmp(path, &rctx->scan_after) <= 0) + { + return NGX_OK; + } + + /* Open cache file */ + ngx_memzero(&f, sizeof(ngx_file_t)); + f.fd = ngx_open_file(path->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, + NGX_FILE_DEFAULT_ACCESS); + if (f.fd == NGX_INVALID_FILE) { + return NGX_OK; /* skip unreadable files */ + } + f.log = ngx_cycle->log; + + if (ngx_fd_info(f.fd, &fi) == NGX_FILE_ERROR) { + ngx_close_file(f.fd); + return NGX_OK; + } + + /* Read binary header */ + n = ngx_read_file(&f, (u_char *) &header, sizeof(header), 0); + if (n < (ssize_t) sizeof(header)) { + ngx_close_file(f.fd); + return NGX_OK; /* skip corrupt/truncated files */ + } + + /* + * Read the cache key from file. + * Layout: [header][\nKEY: ][key_data][\n][HTTP headers...] + * We need to read enough to get the full key. Use header_start as + * upper bound (key ends before HTTP headers start). + */ + if (header.header_start <= sizeof(header) + 6) { + ngx_close_file(f.fd); + return NGX_OK; + } + + key_read_len = header.header_start - sizeof(header) - 6; + if (key_read_len == 0 || key_read_len > 8192) { + ngx_close_file(f.fd); + return NGX_OK; /* invalid or too long */ + } + + key_buf = ngx_pnalloc(rctx->request->pool, key_read_len + 1); + if (key_buf == NULL) { + ngx_close_file(f.fd); + return NGX_OK; + } + + n = ngx_read_file(&f, key_buf, key_read_len, + sizeof(header) + 6); /* skip header + "\nKEY: " */ + ngx_close_file(f.fd); + + if (n < 1) { + return NGX_OK; + } + + /* Null-terminate and strip trailing LF */ + key_buf[n] = '\0'; + if (n > 0 && key_buf[n - 1] == LF) { + key_buf[n - 1] = '\0'; + n--; + } + + if (exact_match) { + if ((size_t) n != rctx->key_partial.len + || ngx_strncasecmp(key_buf, rctx->key_partial.data, + rctx->key_partial.len) != 0) + { + return NGX_OK; + } + + } else if (!rctx->purge_all && rctx->key_partial.len > 0) { + /* Check if key matches our partial prefix */ + if ((size_t) n < rctx->key_partial.len) { + return NGX_OK; /* key too short to match */ + } + if (ngx_strncasecmp(key_buf, rctx->key_partial.data, + rctx->key_partial.len) != 0) + { + return NGX_OK; /* no match */ + } + } + + if ((size_t) n < rctx->key_prefix_len) { + return NGX_OK; + } + + /* Match found — add to current chunk */ + if (rctx->queued >= rctx->chunk_limit) { + return NGX_ABORT; + } + + file = ngx_array_push(rctx->files); + if (file == NULL) { + return NGX_OK; + } + + /* Store cache file path */ + file->path.len = path->len; + file->path.data = ngx_pnalloc(rctx->chunk_pool, path->len + 1); + if (file->path.data == NULL) { + return NGX_OK; + } + ngx_memcpy(file->path.data, path->data, path->len); + file->path.data[path->len] = '\0'; + + file->item.cache_path = file->path; + + /* Extract URI from key by removing the non-URI prefix */ + p = key_buf + rctx->key_prefix_len; + + /* Split URI and args at '?' */ + q = (u_char *) ngx_strchr(p, '?'); + if (q != NULL) { + file->uri.len = q - p; + file->uri.data = ngx_pnalloc(rctx->chunk_pool, file->uri.len + 1); + if (file->uri.data) { + ngx_memcpy(file->uri.data, p, file->uri.len); + file->uri.data[file->uri.len] = '\0'; + } + q++; /* skip '?' */ + file->args.len = n - rctx->key_prefix_len - file->uri.len - 1; + file->args.data = ngx_pnalloc(rctx->chunk_pool, file->args.len + 1); + if (file->args.data) { + ngx_memcpy(file->args.data, q, file->args.len); + file->args.data[file->args.len] = '\0'; + } + } else { + file->uri.len = n - rctx->key_prefix_len; + file->uri.data = ngx_pnalloc(rctx->chunk_pool, file->uri.len + 1); + if (file->uri.data) { + ngx_memcpy(file->uri.data, p, file->uri.len); + file->uri.data[file->uri.len] = '\0'; + } + file->args.len = 0; + file->args.data = NULL; + } + + /* Store ETag from binary header */ + if (header.etag_len > 0 && header.etag_len < NGX_HTTP_CACHE_ETAG_LEN) { + file->etag.len = header.etag_len; + file->etag.data = ngx_pnalloc(rctx->chunk_pool, header.etag_len + 1); + if (file->etag.data) { + ngx_memcpy(file->etag.data, header.etag, header.etag_len); + file->etag.data[header.etag_len] = '\0'; + } + } else { + file->etag.len = 0; + file->etag.data = NULL; + } + + file->item.etag_len = file->etag.len; + if (file->etag.len > 0) { + ngx_memcpy(file->item.etag, file->etag.data, file->etag.len); + } + + /* Store Last-Modified */ + file->last_modified = header.last_modified; + file->item.last_modified = header.last_modified; + file->item.fs_size = ngx_file_size(&fi); + + file->item.cache_key.len = n; + file->item.cache_key.data = ngx_pnalloc(rctx->chunk_pool, n + 1); + if (file->item.cache_key.data == NULL) { + return NGX_OK; + } + ngx_memcpy(file->item.cache_key.data, key_buf, n); + file->item.cache_key.data[n] = '\0'; + + rctx->queued++; + rctx->total++; + + rctx->resume_path.len = path->len; + rctx->resume_path.data = ngx_pnalloc(rctx->request->pool, path->len + 1); + if (rctx->resume_path.data != NULL) { + ngx_memcpy(rctx->resume_path.data, path->data, path->len); + rctx->resume_path.data[path->len] = '\0'; + } + + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, ngx_cycle->log, 0, + "refresh collect: uri=\"%V\" etag=\"%V\" path=\"%V\"", + &file->uri, &file->etag, &file->path); + + return NGX_OK; +} + + +/* + * Subrequest post-handler: called when a refresh HEAD subrequest completes. + * Checks upstream status to decide whether to keep or purge the cache file. + */ +static ngx_int_t +ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, + ngx_int_t rc) +{ + ngx_http_cache_purge_refresh_post_data_t *pd; + ngx_http_cache_purge_refresh_ctx_t *ctx; + ngx_http_cache_purge_refresh_file_t *file; + ngx_http_cache_purge_invalidate_result_e invalidate_result; + ngx_pool_t *pool; + ngx_uint_t status; + + pd = data; + ctx = pd->ctx; + file = pd->file; + + /* + * Determine upstream response status. + * If upstream returned a response, check status_n. + * If subrequest failed (timeout, connect error), status_n will be 0. + */ + status = 0; + if (r->upstream && r->upstream->headers_in.status_n) { + status = r->upstream->headers_in.status_n; + } else if (r->headers_out.status) { + status = r->headers_out.status; + } + + if (status == NGX_HTTP_NOT_MODIFIED) { + /* 304 — cache is still fresh, keep it */ + ctx->refreshed++; + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "refresh: 304 kept \"%V\"", &file->uri); + } else if (status == NGX_HTTP_OK) { + /* 200 — content changed, invalidate through unified helper */ + pool = ngx_create_pool(4096, r->connection->log); + if (pool == NULL) { + ctx->errors++; + } else if (ngx_http_cache_purge_invalidate_item(ctx->request, + ctx->cache, + pool, + &file->item, + &invalidate_result) + != NGX_OK) + { + ctx->errors++; + ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, + "refresh invalidate failed for \"%V\"", &file->uri); + } else if (invalidate_result + == NGX_HTTP_CACHE_PURGE_INVALIDATE_PURGED) + { + ctx->purged++; + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "refresh: 200 purged \"%V\"", &file->uri); + } else if (invalidate_result + == NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING + || invalidate_result + == NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_REPLACED) + { + ctx->refreshed++; + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "refresh: 200 race-kept (%ui) \"%V\"", + invalidate_result, &file->uri); + } else { + ctx->errors++; + } + + if (pool != NULL) { + ngx_destroy_pool(pool); + } + } else { + /* Error or unexpected status — keep cache (conservative) */ + ctx->errors++; + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "refresh: %ui error kept \"%V\"", status, &file->uri); + } + + ctx->active--; + + if (ctx->timed_out) { + if (ctx->current < ctx->queued) { + ctx->errors += ctx->queued - ctx->current; + ctx->current = ctx->queued; + } + + if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + + return NGX_OK; + } + + if (ctx->current < ctx->queued) { + ngx_http_cache_purge_refresh_fire_subrequest(ctx->request, ctx); + } else if (!ctx->scan_done && ctx->active == 0) { + if (ngx_http_cache_purge_refresh_scan_next_chunk(ctx->request, ctx) + != NGX_OK) + { + ctx->errors++; + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + return NGX_OK; + } + + if (ctx->queued > 0) { + ngx_http_cache_purge_refresh_fire_subrequest(ctx->request, ctx); + } else if (ctx->scan_done) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + } else if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + + return NGX_OK; +} + + +/* + * Fire a single HEAD subrequest for the next file in the queue. + */ +static ngx_int_t +ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, + ngx_http_cache_purge_refresh_ctx_t *ctx) +{ + ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_http_cache_purge_refresh_file_t *file; + ngx_http_cache_purge_refresh_post_data_t *pd; + ngx_http_request_t *sr; + ngx_http_post_subrequest_t *ps; + ngx_int_t rc; + ngx_table_elt_t *h; + u_char *time_buf; + + if (ctx->current >= ctx->queued) { + return NGX_OK; + } + + cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); + + if (cplcf->refresh_timeout != 0 && ngx_current_msec >= ctx->deadline) { + ngx_http_cache_purge_refresh_mark_timeout(ctx); + return NGX_ABORT; + } + + file = (ngx_http_cache_purge_refresh_file_t *)ctx->files->elts + + ctx->current; + ctx->current++; + + /* Allocate post-subrequest callback with wrapper data */ + ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t)); + if (ps == NULL) { + return NGX_ERROR; + } + + pd = ngx_palloc(r->pool, sizeof(ngx_http_cache_purge_refresh_post_data_t)); + if (pd == NULL) { + return NGX_ERROR; + } + pd->ctx = ctx; + pd->file = file; + + ps->handler = ngx_http_cache_purge_refresh_done; + ps->data = pd; + + /* Create subrequest */ + rc = ngx_http_subrequest(r, &file->uri, + file->args.len > 0 ? &file->args : NULL, + &sr, ps, + NGX_HTTP_SUBREQUEST_WAITED); + if (rc != NGX_OK) { + ctx->errors++; + return rc; + } + + /* Change method to HEAD */ + sr->method = NGX_HTTP_HEAD; + sr->method_name = ngx_http_head_method_name; + sr->header_only = 1; + + /* + * Inject conditional headers for cache validation. + * + * Subrequest's headers_in.headers list is shared with parent. + * We must re-initialize it as an independent list so that pushing + * conditional headers does not corrupt the parent's header list. + * We copy essential headers (Host) and add conditional headers. + */ + if (ngx_list_init(&sr->headers_in.headers, r->pool, 8, + sizeof(ngx_table_elt_t)) + != NGX_OK) + { + ctx->errors++; + return NGX_ERROR; + } + + /* Copy Host header from parent — required by upstream */ + if (r->headers_in.host != NULL) { + h = ngx_list_push(&sr->headers_in.headers); + if (h != NULL) { + *h = *r->headers_in.host; + sr->headers_in.host = h; + } + } + + /* Clear inherited shortcut pointers that reference parent's headers */ + sr->headers_in.if_none_match = NULL; + sr->headers_in.if_modified_since = NULL; + + /* If-None-Match (ETag) */ + if (file->etag.len > 0) { + h = ngx_list_push(&sr->headers_in.headers); + if (h != NULL) { + h->hash = 1; + ngx_str_set(&h->key, "If-None-Match"); + h->value = file->etag; + h->lowcase_key = (u_char *) "if-none-match"; + sr->headers_in.if_none_match = h; + } + } + + /* If-Modified-Since */ + if (file->last_modified > 0) { + h = ngx_list_push(&sr->headers_in.headers); + if (h != NULL) { + time_buf = ngx_pnalloc(r->pool, + sizeof("Mon, 28 Sep 1970 06:00:00 GMT")); + if (time_buf != NULL) { + h->hash = 1; + ngx_str_set(&h->key, "If-Modified-Since"); + h->value.data = time_buf; + ngx_http_time(time_buf, file->last_modified); + h->value.len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1; + h->lowcase_key = (u_char *) "if-modified-since"; + sr->headers_in.if_modified_since = h; + } + } + } + + ctx->active++; + + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "refresh: fired subrequest for \"%V\" (%ui/%ui)", + &file->uri, ctx->current, ctx->queued); + + return NGX_OK; +} + + +/* + * Build and send the final refresh response to the client. + */ +static ngx_int_t +ngx_http_cache_purge_refresh_send_response(ngx_http_request_t *r) +{ + ngx_http_cache_purge_refresh_ctx_t *ctx; + ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_buf_t *b; + ngx_chain_t out; + ngx_int_t rc; + size_t len; + u_char *p; + + ctx = ngx_http_get_module_ctx(r, ngx_http_cache_purge_module); + cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); + + /* Calculate response body size */ + if (cplcf->resptype == NGX_REPONSE_TYPE_JSON) { + /* JSON: {"status":"refresh","total":N,"kept":N,"purged":N,"errors":N} */ + len = sizeof("{\"status\":\"refresh\",\"total\":,\"kept\":,\"purged\":,\"errors\":}") + + 4 * NGX_INT_T_LEN; + + r->headers_out.content_type.len = ngx_http_cache_purge_content_type_json_size - 1; + r->headers_out.content_type.data = (u_char *) ngx_http_cache_purge_content_type_json; + } else { + /* Text: "Refresh: total=N kept=N purged=N errors=N\n" */ + len = sizeof("Refresh: total= kept= purged= errors=\n") + + 4 * NGX_INT_T_LEN; + + r->headers_out.content_type.len = ngx_http_cache_purge_content_type_text_size - 1; + r->headers_out.content_type.data = (u_char *) ngx_http_cache_purge_content_type_text; + } + + b = ngx_create_temp_buf(r->pool, len); + if (b == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (cplcf->resptype == NGX_REPONSE_TYPE_JSON) { + p = ngx_sprintf(b->pos, + "{\"status\":\"refresh\",\"total\":%ui,\"kept\":%ui," + "\"purged\":%ui,\"errors\":%ui}", + ctx->total, ctx->refreshed, ctx->purged, ctx->errors); + } else { + p = ngx_sprintf(b->pos, + "Refresh: total=%ui kept=%ui purged=%ui errors=%ui\n", + ctx->total, ctx->refreshed, ctx->purged, ctx->errors); + } + + b->last = p; + b->last_buf = 1; + b->last_in_chain = 1; + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = p - b->pos; + + rc = ngx_http_send_header(r); + if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { + return rc; + } + + out.buf = b; + out.next = NULL; + + return ngx_http_output_filter(r, &out); +} + + +static void +ngx_http_cache_purge_refresh_mark_timeout(ngx_http_cache_purge_refresh_ctx_t *ctx) +{ + ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_uint_t skipped; + + if (ctx->timed_out) { + return; + } + + ctx->timed_out = 1; + cplcf = ngx_http_get_module_loc_conf(ctx->request, + ngx_http_cache_purge_module); + + if (ctx->current < ctx->total) { + skipped = ctx->total - ctx->current; + ctx->errors += skipped; + ctx->current = ctx->total; + + } else { + skipped = 0; + } + + ngx_log_error(NGX_LOG_ERR, ctx->request->connection->log, 0, + "cache purge refresh timed out after %M ms, skipped %ui pending entries", + cplcf->refresh_timeout, + skipped); +} + + +static void +ngx_http_cache_purge_refresh_finalize(ngx_http_request_t *r, + ngx_http_cache_purge_refresh_ctx_t *ctx) +{ + if (ctx->finalized) { + return; + } + + ctx->finalized = 1; + + if (ctx->timeout_ev.timer_set) { + ngx_del_timer(&ctx->timeout_ev); + } + + ngx_http_finalize_request(r, ngx_http_cache_purge_refresh_send_response(r)); +} + + +static ngx_int_t +ngx_http_cache_purge_refresh_scan_next_chunk(ngx_http_request_t *r, + ngx_http_cache_purge_refresh_ctx_t *ctx) +{ + ngx_tree_ctx_t tree; + ngx_int_t rc; + + if (ctx->scan_done) { + return NGX_OK; + } + + if (ctx->chunk_pool != NULL) { + ngx_destroy_pool(ctx->chunk_pool); + } + + ctx->chunk_pool = ngx_create_pool(4096, r->connection->log); + if (ctx->chunk_pool == NULL) { + return NGX_ERROR; + } + + ctx->files = ngx_array_create(ctx->chunk_pool, ctx->chunk_limit, + sizeof(ngx_http_cache_purge_refresh_file_t)); + if (ctx->files == NULL) { + return NGX_ERROR; + } + + ctx->current = 0; + ctx->queued = 0; + ctx->scan_after = ctx->resume_path; + + if (ctx->exact) { + ctx->scan_done = 1; + return ngx_http_cache_purge_refresh_collect_open_file(r, ctx); + } + + tree.init_handler = NULL; + tree.file_handler = ngx_http_cache_purge_refresh_collect_file; + tree.pre_tree_handler = ngx_http_purge_file_cache_noop; + tree.post_tree_handler = ngx_http_purge_file_cache_noop; + tree.spec_handler = ngx_http_purge_file_cache_noop; + tree.data = ctx; + tree.alloc = 0; + tree.log = ngx_cycle->log; + + rc = ngx_walk_tree(&tree, &ctx->cache->path->name); + if (rc == NGX_ABORT) { + return NGX_OK; + } + + if (rc != NGX_OK) { + return rc; + } + + ctx->scan_done = 1; + return NGX_OK; +} + + +static void +ngx_http_cache_purge_refresh_timeout_handler(ngx_event_t *ev) +{ + ngx_http_cache_purge_refresh_ctx_t *ctx; + + ctx = ev->data; + + if (ctx == NULL || ctx->finalized) { + return; + } + + ngx_http_cache_purge_refresh_mark_timeout(ctx); + + if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } +} + + +/* + * Start firing subrequests for the current chunk. + */ +static void +ngx_http_cache_purge_refresh_start(ngx_http_request_t *r) +{ + ngx_http_cache_purge_refresh_ctx_t *ctx; + ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_uint_t i, concurrency; + + ctx = ngx_http_get_module_ctx(r, ngx_http_cache_purge_module); + + if (ctx == NULL) { + ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + if (ctx->queued == 0 && ctx->scan_done && ctx->total == 0) { + ngx_http_cache_purge_refresh_finalize(r, ctx); + return; + } + + if (ctx->current >= ctx->queued && ctx->active == 0 && ctx->scan_done) { + ngx_http_cache_purge_refresh_finalize(r, ctx); + return; + } + + if (ctx->timed_out) { + return; + } + + if (ctx->queued == 0 && !ctx->scan_done) { + if (ngx_http_cache_purge_refresh_scan_next_chunk(r, ctx) != NGX_OK) { + ctx->errors++; + ngx_http_cache_purge_refresh_finalize(r, ctx); + return; + } + } + + if (ctx->queued == 0) { + if (ctx->scan_done) { + ngx_http_cache_purge_refresh_finalize(r, ctx); + } + return; + } + + cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); + concurrency = cplcf->refresh_concurrency; + if (concurrency > ctx->queued) { + concurrency = ctx->queued; + } + + for (i = ctx->active; i < concurrency; i++) { + if (ngx_http_cache_purge_refresh_fire_subrequest(r, ctx) != NGX_OK) { + break; + } + } +} + + +/* + * Main entry point for the refresh feature. + * Called from backend purge handlers when refresh mode is active. + * Supports exact refresh, partial refresh, and refresh_all. + */ +static ngx_int_t +ngx_http_cache_purge_refresh(ngx_http_request_t *r, + ngx_http_file_cache_t *cache) +{ + ngx_http_cache_purge_refresh_ctx_t *ctx; + ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_str_t *keys; + ngx_str_t key; + ngx_str_t tail; + + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "cache purge refresh in %s", cache->path->name.data); + + /* Allocate refresh context */ + ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_cache_purge_refresh_ctx_t)); + if (ctx == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ctx->request = r; + ctx->cache = cache; + + cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); + ctx->purge_all = cplcf->conf->purge_all; + ctx->exact = !ctx->purge_all && !ngx_http_cache_purge_is_partial(r); + ctx->deadline = ngx_current_msec + cplcf->refresh_timeout; + + ngx_memzero(&ctx->timeout_ev, sizeof(ngx_event_t)); + ctx->timeout_ev.handler = ngx_http_cache_purge_refresh_timeout_handler; + ctx->timeout_ev.data = ctx; + ctx->timeout_ev.log = r->connection->log; + + /* Get the evaluated cache key. Strip trailing '*' only for partial refresh. */ + keys = r->cache->keys.elts; + key = keys[0]; + if (!ctx->exact && key.len > 0 && key.data[key.len - 1] == '*') { + key.len--; + } + + /* Store exact key or partial prefix for later matching. */ + ctx->key_partial.len = key.len; + ctx->key_partial.data = key.data; + + /* + * Infer the non-URI prefix in cache key. + * We first try unparsed_uri (keeps args), then uri. + * If neither is at key tail, fall back to 0. + */ + tail = r->unparsed_uri; + if (!ctx->exact && tail.len > 0 && tail.data[tail.len - 1] == '*') { + tail.len--; + } + + if (tail.len > 0 && key.len >= tail.len + && ngx_strncasecmp(key.data + key.len - tail.len, tail.data, + tail.len) == 0) + { + ctx->key_prefix_len = key.len - tail.len; + + } else { + tail = r->uri; + if (!ctx->exact && tail.len > 0 && tail.data[tail.len - 1] == '*') { + tail.len--; + } + + if (tail.len > 0 && key.len >= tail.len + && ngx_strncasecmp(key.data + key.len - tail.len, tail.data, + tail.len) == 0) + { + ctx->key_prefix_len = key.len - tail.len; + + } else { + ctx->key_prefix_len = 0; + } + } + + ctx->chunk_limit = cplcf->refresh_concurrency * 4; + if (ctx->chunk_limit < cplcf->refresh_concurrency) { + ctx->chunk_limit = cplcf->refresh_concurrency; + } + if (ctx->chunk_limit < 1024) { + ctx->chunk_limit = 1024; + } + + /* Set module context on the request */ + ngx_http_set_ctx(r, ctx, ngx_http_cache_purge_module); + + if (cplcf->refresh_timeout != 0) { + ngx_add_timer(&ctx->timeout_ev, cplcf->refresh_timeout); + } + + /* Set up the write event handler and start chunked subrequests */ + r->write_event_handler = ngx_http_cache_purge_refresh_start; + +#if (nginx_version >= 8011) + r->main->count++; +#endif + + ngx_http_cache_purge_refresh_start(r); + + return NGX_DONE; +} #else /* !NGX_HTTP_CACHE */ static ngx_http_module_t ngx_http_cache_purge_module_ctx = { From e944b8526141a0cc63ab25924d78a266f462edc3 Mon Sep 17 00:00:00 2001 From: yaoge123 Date: Sun, 15 Mar 2026 08:38:03 +0800 Subject: [PATCH 02/43] Remove refresh wildcard chunk-size dependency --- ngx_cache_purge_module.c | 394 ++++++++++++++++++++++++++------------- 1 file changed, 264 insertions(+), 130 deletions(-) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index efa41fe..3540d63 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -366,18 +366,23 @@ typedef struct { ngx_flag_t purge_all; ngx_flag_t exact; ngx_flag_t timed_out; + ngx_flag_t timeout_enabled; ngx_flag_t finalized; ngx_msec_t deadline; ngx_event_t timeout_ev; + + /* collected refresh candidates */ ngx_pool_t *chunk_pool; - ngx_array_t *files; - ngx_uint_t current; - ngx_uint_t queued; - ngx_uint_t active; - ngx_uint_t chunk_limit; - ngx_flag_t scan_done; - ngx_str_t scan_after; - ngx_str_t resume_path; + ngx_array_t *files; /* collected files: ngx_http_cache_purge_refresh_file_t[] */ + ngx_uint_t current; /* next file index to dispatch */ + ngx_uint_t queued; /* total collected file count */ + ngx_uint_t active; /* active subrequest count */ + ngx_uint_t chunk_limit; /* legacy field, no longer on main path */ + ngx_flag_t scan_done; /* collection complete */ + ngx_str_t scan_after; /* lower bound for current scan round */ + ngx_str_t resume_path; /* last collected path for next scan round */ + + /* stats */ ngx_uint_t total; ngx_uint_t refreshed; ngx_uint_t purged; @@ -387,6 +392,7 @@ typedef struct { typedef struct { ngx_http_cache_purge_refresh_ctx_t *ctx; ngx_http_cache_purge_refresh_file_t *file; + ngx_flag_t validation_ready; } ngx_http_cache_purge_refresh_post_data_t; static ngx_int_t ngx_http_cache_purge_refresh(ngx_http_request_t *r, @@ -412,8 +418,6 @@ static void ngx_http_cache_purge_refresh_finalize( ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); static ngx_int_t ngx_http_cache_purge_refresh_scan_next_chunk( ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); -static ngx_int_t ngx_http_cache_purge_refresh_path_cmp( - ngx_str_t *a, ngx_str_t *b); static ngx_int_t ngx_http_cache_purge_add_variable(ngx_conf_t *cf); static ngx_int_t ngx_http_cache_purge_refresh_bypass_variable( ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); @@ -3609,13 +3613,6 @@ ngx_http_cache_purge_refresh_collect_open_file(ngx_http_request_t *r, } -static ngx_int_t -ngx_http_cache_purge_refresh_path_cmp(ngx_str_t *a, ngx_str_t *b) -{ - return ngx_memn2cmp(a->data, b->data, a->len, b->len); -} - - static ngx_int_t ngx_http_cache_purge_refresh_collect_path( ngx_http_cache_purge_refresh_ctx_t *rctx, ngx_str_t *path, @@ -3625,15 +3622,30 @@ ngx_http_cache_purge_refresh_collect_path( ngx_http_file_cache_header_t header; ngx_file_t f; ngx_file_info_t fi; + ngx_str_t path_copy, uri, args, etag, cache_key; + ngx_http_cache_purge_invalidate_item_t item; u_char *key_buf; + u_char *path_data; + u_char *uri_data; + u_char *args_data; + u_char *etag_data; + u_char *cache_key_data; ssize_t n; size_t key_read_len; u_char *p, *q; - if (!exact_match && rctx->scan_after.len > 0 - && ngx_http_cache_purge_refresh_path_cmp(path, &rctx->scan_after) <= 0) + ngx_memzero(&path_copy, sizeof(ngx_str_t)); + ngx_memzero(&uri, sizeof(ngx_str_t)); + ngx_memzero(&args, sizeof(ngx_str_t)); + ngx_memzero(&etag, sizeof(ngx_str_t)); + ngx_memzero(&cache_key, sizeof(ngx_str_t)); + ngx_memzero(&item, sizeof(ngx_http_cache_purge_invalidate_item_t)); + + if (rctx->timeout_enabled && !rctx->timed_out + && ngx_current_msec >= rctx->deadline) { - return NGX_OK; + ngx_http_cache_purge_refresh_mark_timeout(rctx); + return NGX_ABORT; } /* Open cache file */ @@ -3674,10 +3686,10 @@ ngx_http_cache_purge_refresh_collect_path( return NGX_OK; /* invalid or too long */ } - key_buf = ngx_pnalloc(rctx->request->pool, key_read_len + 1); + key_buf = ngx_alloc(key_read_len + 1, ngx_cycle->log); if (key_buf == NULL) { ngx_close_file(f.fd); - return NGX_OK; + return NGX_ERROR; } n = ngx_read_file(&f, key_buf, key_read_len, @@ -3700,45 +3712,39 @@ ngx_http_cache_purge_refresh_collect_path( || ngx_strncasecmp(key_buf, rctx->key_partial.data, rctx->key_partial.len) != 0) { + ngx_free(key_buf); return NGX_OK; } } else if (!rctx->purge_all && rctx->key_partial.len > 0) { /* Check if key matches our partial prefix */ if ((size_t) n < rctx->key_partial.len) { + ngx_free(key_buf); return NGX_OK; /* key too short to match */ } if (ngx_strncasecmp(key_buf, rctx->key_partial.data, rctx->key_partial.len) != 0) { + ngx_free(key_buf); return NGX_OK; /* no match */ } } if ((size_t) n < rctx->key_prefix_len) { + ngx_free(key_buf); return NGX_OK; } - /* Match found — add to current chunk */ - if (rctx->queued >= rctx->chunk_limit) { - return NGX_ABORT; - } - - file = ngx_array_push(rctx->files); - if (file == NULL) { - return NGX_OK; - } - - /* Store cache file path */ - file->path.len = path->len; - file->path.data = ngx_pnalloc(rctx->chunk_pool, path->len + 1); - if (file->path.data == NULL) { - return NGX_OK; + path_data = ngx_pnalloc(rctx->request->pool, path->len + 1); + if (path_data == NULL) { + ngx_free(key_buf); + return NGX_ERROR; } - ngx_memcpy(file->path.data, path->data, path->len); - file->path.data[path->len] = '\0'; - - file->item.cache_path = file->path; + ngx_memcpy(path_data, path->data, path->len); + path_data[path->len] = '\0'; + path_copy.len = path->len; + path_copy.data = path_data; + item.cache_path = path_copy; /* Extract URI from key by removing the non-URI prefix */ p = key_buf + rctx->key_prefix_len; @@ -3746,71 +3752,88 @@ ngx_http_cache_purge_refresh_collect_path( /* Split URI and args at '?' */ q = (u_char *) ngx_strchr(p, '?'); if (q != NULL) { - file->uri.len = q - p; - file->uri.data = ngx_pnalloc(rctx->chunk_pool, file->uri.len + 1); - if (file->uri.data) { - ngx_memcpy(file->uri.data, p, file->uri.len); - file->uri.data[file->uri.len] = '\0'; + uri.len = q - p; + uri_data = ngx_pnalloc(rctx->request->pool, uri.len + 1); + if (uri_data == NULL) { + ngx_free(key_buf); + return NGX_ERROR; } + ngx_memcpy(uri_data, p, uri.len); + uri_data[uri.len] = '\0'; + uri.data = uri_data; + q++; /* skip '?' */ - file->args.len = n - rctx->key_prefix_len - file->uri.len - 1; - file->args.data = ngx_pnalloc(rctx->chunk_pool, file->args.len + 1); - if (file->args.data) { - ngx_memcpy(file->args.data, q, file->args.len); - file->args.data[file->args.len] = '\0'; + args.len = n - rctx->key_prefix_len - uri.len - 1; + args_data = ngx_pnalloc(rctx->request->pool, args.len + 1); + if (args_data == NULL) { + ngx_free(key_buf); + return NGX_ERROR; } + ngx_memcpy(args_data, q, args.len); + args_data[args.len] = '\0'; + args.data = args_data; } else { - file->uri.len = n - rctx->key_prefix_len; - file->uri.data = ngx_pnalloc(rctx->chunk_pool, file->uri.len + 1); - if (file->uri.data) { - ngx_memcpy(file->uri.data, p, file->uri.len); - file->uri.data[file->uri.len] = '\0'; + uri.len = n - rctx->key_prefix_len; + uri_data = ngx_pnalloc(rctx->request->pool, uri.len + 1); + if (uri_data == NULL) { + ngx_free(key_buf); + return NGX_ERROR; } - file->args.len = 0; - file->args.data = NULL; + ngx_memcpy(uri_data, p, uri.len); + uri_data[uri.len] = '\0'; + uri.data = uri_data; } /* Store ETag from binary header */ if (header.etag_len > 0 && header.etag_len < NGX_HTTP_CACHE_ETAG_LEN) { - file->etag.len = header.etag_len; - file->etag.data = ngx_pnalloc(rctx->chunk_pool, header.etag_len + 1); - if (file->etag.data) { - ngx_memcpy(file->etag.data, header.etag, header.etag_len); - file->etag.data[header.etag_len] = '\0'; + etag_data = ngx_pnalloc(rctx->request->pool, header.etag_len + 1); + if (etag_data == NULL) { + ngx_free(key_buf); + return NGX_ERROR; } - } else { - file->etag.len = 0; - file->etag.data = NULL; + ngx_memcpy(etag_data, header.etag, header.etag_len); + etag_data[header.etag_len] = '\0'; + etag.len = header.etag_len; + etag.data = etag_data; } - file->item.etag_len = file->etag.len; - if (file->etag.len > 0) { - ngx_memcpy(file->item.etag, file->etag.data, file->etag.len); + item.etag_len = etag.len; + if (etag.len > 0) { + ngx_memcpy(item.etag, etag.data, etag.len); } /* Store Last-Modified */ - file->last_modified = header.last_modified; - file->item.last_modified = header.last_modified; - file->item.fs_size = ngx_file_size(&fi); + item.last_modified = header.last_modified; + item.fs_size = ngx_file_size(&fi); - file->item.cache_key.len = n; - file->item.cache_key.data = ngx_pnalloc(rctx->chunk_pool, n + 1); - if (file->item.cache_key.data == NULL) { - return NGX_OK; + cache_key_data = ngx_pnalloc(rctx->request->pool, n + 1); + if (cache_key_data == NULL) { + ngx_free(key_buf); + return NGX_ERROR; } - ngx_memcpy(file->item.cache_key.data, key_buf, n); - file->item.cache_key.data[n] = '\0'; + ngx_memcpy(cache_key_data, key_buf, n); + cache_key_data[n] = '\0'; + cache_key.len = n; + cache_key.data = cache_key_data; + item.cache_key = cache_key; - rctx->queued++; - rctx->total++; + ngx_free(key_buf); - rctx->resume_path.len = path->len; - rctx->resume_path.data = ngx_pnalloc(rctx->request->pool, path->len + 1); - if (rctx->resume_path.data != NULL) { - ngx_memcpy(rctx->resume_path.data, path->data, path->len); - rctx->resume_path.data[path->len] = '\0'; + file = ngx_array_push(rctx->files); + if (file == NULL) { + return NGX_ERROR; } + file->path = path_copy; + file->uri = uri; + file->args = args; + file->etag = etag; + file->last_modified = header.last_modified; + file->item = item; + + rctx->queued++; + rctx->total++; + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, ngx_cycle->log, 0, "refresh collect: uri=\"%V\" etag=\"%V\" path=\"%V\"", &file->uri, &file->etag, &file->path); @@ -3838,6 +3861,39 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, ctx = pd->ctx; file = pd->file; + if (!pd->validation_ready) { + ctx->errors++; + if (ctx->active > 0) { + ctx->active--; + } + + if (ctx->timed_out) { + if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + return NGX_OK; + } + + if (ctx->current < ctx->queued) { + ngx_int_t fire_rc; + + fire_rc = ngx_http_cache_purge_refresh_fire_subrequest( + ctx->request, ctx); + if (fire_rc == NGX_ABORT && ctx->timed_out) { + if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + } else if (fire_rc != NGX_OK) { + ctx->errors += ctx->queued - ctx->current; + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + } else if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + + return NGX_OK; + } + /* * Determine upstream response status. * If upstream returned a response, check status_n. @@ -3915,7 +3971,18 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, } if (ctx->current < ctx->queued) { - ngx_http_cache_purge_refresh_fire_subrequest(ctx->request, ctx); + ngx_int_t fire_rc; + + fire_rc = ngx_http_cache_purge_refresh_fire_subrequest(ctx->request, + ctx); + if (fire_rc == NGX_ABORT && ctx->timed_out) { + if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + } else if (fire_rc != NGX_OK) { + ctx->errors += ctx->queued - ctx->current; + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } } else if (!ctx->scan_done && ctx->active == 0) { if (ngx_http_cache_purge_refresh_scan_next_chunk(ctx->request, ctx) != NGX_OK) @@ -3926,7 +3993,18 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, } if (ctx->queued > 0) { - ngx_http_cache_purge_refresh_fire_subrequest(ctx->request, ctx); + ngx_int_t fire_rc; + + fire_rc = ngx_http_cache_purge_refresh_fire_subrequest( + ctx->request, ctx); + if (fire_rc == NGX_ABORT && ctx->timed_out) { + if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } + } else if (fire_rc != NGX_OK) { + ctx->errors += ctx->queued - ctx->current; + ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); + } } else if (ctx->scan_done) { ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); } @@ -3967,7 +4045,6 @@ ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, file = (ngx_http_cache_purge_refresh_file_t *)ctx->files->elts + ctx->current; - ctx->current++; /* Allocate post-subrequest callback with wrapper data */ ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t)); @@ -3981,6 +4058,7 @@ ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, } pd->ctx = ctx; pd->file = file; + pd->validation_ready = 0; ps->handler = ngx_http_cache_purge_refresh_done; ps->data = pd; @@ -3995,6 +4073,9 @@ ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, return rc; } + ctx->current++; + ctx->active++; + /* Change method to HEAD */ sr->method = NGX_HTTP_HEAD; sr->method_name = ngx_http_head_method_name; @@ -4013,16 +4094,18 @@ ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, != NGX_OK) { ctx->errors++; - return NGX_ERROR; + return NGX_OK; } /* Copy Host header from parent — required by upstream */ if (r->headers_in.host != NULL) { h = ngx_list_push(&sr->headers_in.headers); - if (h != NULL) { - *h = *r->headers_in.host; - sr->headers_in.host = h; + if (h == NULL) { + ctx->errors++; + return NGX_OK; } + *h = *r->headers_in.host; + sr->headers_in.host = h; } /* Clear inherited shortcut pointers that reference parent's headers */ @@ -4032,34 +4115,40 @@ ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, /* If-None-Match (ETag) */ if (file->etag.len > 0) { h = ngx_list_push(&sr->headers_in.headers); - if (h != NULL) { - h->hash = 1; - ngx_str_set(&h->key, "If-None-Match"); - h->value = file->etag; - h->lowcase_key = (u_char *) "if-none-match"; - sr->headers_in.if_none_match = h; + if (h == NULL) { + ctx->errors++; + return NGX_OK; } + h->hash = 1; + ngx_str_set(&h->key, "If-None-Match"); + h->value = file->etag; + h->lowcase_key = (u_char *) "if-none-match"; + sr->headers_in.if_none_match = h; } /* If-Modified-Since */ if (file->last_modified > 0) { h = ngx_list_push(&sr->headers_in.headers); - if (h != NULL) { - time_buf = ngx_pnalloc(r->pool, - sizeof("Mon, 28 Sep 1970 06:00:00 GMT")); - if (time_buf != NULL) { - h->hash = 1; - ngx_str_set(&h->key, "If-Modified-Since"); - h->value.data = time_buf; - ngx_http_time(time_buf, file->last_modified); - h->value.len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1; - h->lowcase_key = (u_char *) "if-modified-since"; - sr->headers_in.if_modified_since = h; - } + if (h == NULL) { + ctx->errors++; + return NGX_OK; + } + time_buf = ngx_pnalloc(r->pool, + sizeof("Mon, 28 Sep 1970 06:00:00 GMT")); + if (time_buf == NULL) { + ctx->errors++; + return NGX_OK; } + h->hash = 1; + ngx_str_set(&h->key, "If-Modified-Since"); + h->value.data = time_buf; + ngx_http_time(time_buf, file->last_modified); + h->value.len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1; + h->lowcase_key = (u_char *) "if-modified-since"; + sr->headers_in.if_modified_since = h; } - ctx->active++; + pd->validation_ready = 1; ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "refresh: fired subrequest for \"%V\" (%ui/%ui)", @@ -4264,13 +4353,14 @@ ngx_http_cache_purge_refresh_timeout_handler(ngx_event_t *ev) /* - * Start firing subrequests for the current chunk. + * Start firing subrequests for the collected file set. */ static void ngx_http_cache_purge_refresh_start(ngx_http_request_t *r) { ngx_http_cache_purge_refresh_ctx_t *ctx; ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_int_t rc; ngx_uint_t i, concurrency; ctx = ngx_http_get_module_ctx(r, ngx_http_cache_purge_module); @@ -4291,15 +4381,10 @@ ngx_http_cache_purge_refresh_start(ngx_http_request_t *r) } if (ctx->timed_out) { - return; - } - - if (ctx->queued == 0 && !ctx->scan_done) { - if (ngx_http_cache_purge_refresh_scan_next_chunk(r, ctx) != NGX_OK) { - ctx->errors++; + if (ctx->active == 0) { ngx_http_cache_purge_refresh_finalize(r, ctx); - return; } + return; } if (ctx->queued == 0) { @@ -4311,14 +4396,26 @@ ngx_http_cache_purge_refresh_start(ngx_http_request_t *r) cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); concurrency = cplcf->refresh_concurrency; + if (concurrency == 0) { + concurrency = 1; + } if (concurrency > ctx->queued) { concurrency = ctx->queued; } for (i = ctx->active; i < concurrency; i++) { - if (ngx_http_cache_purge_refresh_fire_subrequest(r, ctx) != NGX_OK) { + rc = ngx_http_cache_purge_refresh_fire_subrequest(r, ctx); + if (rc == NGX_OK) { + continue; + } + + if (rc == NGX_ABORT && ctx->timed_out) { break; } + + ctx->errors += ctx->queued - ctx->current; + ngx_http_cache_purge_refresh_finalize(r, ctx); + return; } } @@ -4334,9 +4431,11 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, { ngx_http_cache_purge_refresh_ctx_t *ctx; ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_tree_ctx_t tree; ngx_str_t *keys; ngx_str_t key; ngx_str_t tail; + ngx_int_t rc; ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "cache purge refresh in %s", cache->path->name.data); @@ -4353,6 +4452,7 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); ctx->purge_all = cplcf->conf->purge_all; ctx->exact = !ctx->purge_all && !ngx_http_cache_purge_is_partial(r); + ctx->timeout_enabled = (cplcf->refresh_timeout != 0); ctx->deadline = ngx_current_msec + cplcf->refresh_timeout; ngx_memzero(&ctx->timeout_ev, sizeof(ngx_event_t)); @@ -4360,6 +4460,10 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, ctx->timeout_ev.data = ctx; ctx->timeout_ev.log = r->connection->log; + if (ctx->timeout_enabled) { + ngx_add_timer(&ctx->timeout_ev, cplcf->refresh_timeout); + } + /* Get the evaluated cache key. Strip trailing '*' only for partial refresh. */ keys = r->cache->keys.elts; key = keys[0]; @@ -4404,22 +4508,52 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, } } - ctx->chunk_limit = cplcf->refresh_concurrency * 4; - if (ctx->chunk_limit < cplcf->refresh_concurrency) { - ctx->chunk_limit = cplcf->refresh_concurrency; + ctx->files = ngx_array_create(r->pool, 256, + sizeof(ngx_http_cache_purge_refresh_file_t)); + if (ctx->files == NULL) { + if (ctx->timeout_ev.timer_set) { + ngx_del_timer(&ctx->timeout_ev); + } + return NGX_HTTP_INTERNAL_SERVER_ERROR; } - if (ctx->chunk_limit < 1024) { - ctx->chunk_limit = 1024; + + if (ctx->exact) { + rc = ngx_http_cache_purge_refresh_collect_open_file(r, ctx); + if (rc == NGX_ABORT && ctx->timed_out) { + ctx->scan_done = 1; + } else if (rc != NGX_OK) { + if (ctx->timeout_ev.timer_set) { + ngx_del_timer(&ctx->timeout_ev); + } + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + } else { + tree.init_handler = NULL; + tree.file_handler = ngx_http_cache_purge_refresh_collect_file; + tree.pre_tree_handler = ngx_http_purge_file_cache_noop; + tree.post_tree_handler = ngx_http_purge_file_cache_noop; + tree.spec_handler = ngx_http_purge_file_cache_noop; + tree.data = ctx; + tree.alloc = 0; + tree.log = ngx_cycle->log; + + rc = ngx_walk_tree(&tree, &ctx->cache->path->name); + if (rc == NGX_ABORT && ctx->timed_out) { + ctx->scan_done = 1; + } else if (rc != NGX_OK) { + if (ctx->timeout_ev.timer_set) { + ngx_del_timer(&ctx->timeout_ev); + } + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } } + ctx->scan_done = 1; + /* Set module context on the request */ ngx_http_set_ctx(r, ctx, ngx_http_cache_purge_module); - if (cplcf->refresh_timeout != 0) { - ngx_add_timer(&ctx->timeout_ev, cplcf->refresh_timeout); - } - - /* Set up the write event handler and start chunked subrequests */ + /* Set up the write event handler and start bounded subrequests */ r->write_event_handler = ngx_http_cache_purge_refresh_start; #if (nginx_version >= 8011) From 0812c11ad4a20c64ba70cef96c591e796da9b642 Mon Sep 17 00:00:00 2001 From: yaoge123 Date: Sun, 15 Mar 2026 11:03:04 +0800 Subject: [PATCH 03/43] Stabilize refresh scanning across production batches --- ngx_cache_purge_module.c | 387 +++++++++++++++++++++++++++++---------- 1 file changed, 292 insertions(+), 95 deletions(-) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index 3540d63..53bd83a 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -359,10 +359,20 @@ typedef struct { } ngx_http_cache_purge_refresh_file_t; typedef struct { - ngx_http_request_t *request; - ngx_http_file_cache_t *cache; - ngx_str_t key_partial; - ngx_uint_t key_prefix_len; + ngx_str_t path; + ngx_flag_t is_dir; +} ngx_http_cache_purge_refresh_scan_entry_t; + +typedef struct { + ngx_queue_t queue; + ngx_str_t path; +} ngx_http_cache_purge_refresh_dir_t; + +typedef struct { + ngx_http_request_t *request; /* parent request */ + ngx_http_file_cache_t *cache; /* cache instance */ + ngx_str_t key_partial; /* key prefix (without *) */ + ngx_uint_t key_prefix_len;/* non-URI prefix length in key */ ngx_flag_t purge_all; ngx_flag_t exact; ngx_flag_t timed_out; @@ -373,14 +383,17 @@ typedef struct { /* collected refresh candidates */ ngx_pool_t *chunk_pool; + ngx_pool_t *scan_pool; ngx_array_t *files; /* collected files: ngx_http_cache_purge_refresh_file_t[] */ + ngx_array_t *scan_entries; /* current directory entries */ ngx_uint_t current; /* next file index to dispatch */ ngx_uint_t queued; /* total collected file count */ ngx_uint_t active; /* active subrequest count */ - ngx_uint_t chunk_limit; /* legacy field, no longer on main path */ + ngx_uint_t chunk_limit; + ngx_uint_t scan_index; ngx_flag_t scan_done; /* collection complete */ - ngx_str_t scan_after; /* lower bound for current scan round */ - ngx_str_t resume_path; /* last collected path for next scan round */ + ngx_flag_t scan_initialized; + ngx_queue_t pending_dirs; /* stats */ ngx_uint_t total; @@ -397,8 +410,6 @@ typedef struct { static ngx_int_t ngx_http_cache_purge_refresh(ngx_http_request_t *r, ngx_http_file_cache_t *cache); -static ngx_int_t ngx_http_cache_purge_refresh_collect_file( - ngx_tree_ctx_t *ctx, ngx_str_t *path); static ngx_int_t ngx_http_cache_purge_refresh_collect_open_file( ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); static ngx_int_t ngx_http_cache_purge_refresh_collect_path( @@ -418,6 +429,11 @@ static void ngx_http_cache_purge_refresh_finalize( ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); static ngx_int_t ngx_http_cache_purge_refresh_scan_next_chunk( ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); +static ngx_int_t ngx_http_cache_purge_refresh_enqueue_dir( + ngx_http_cache_purge_refresh_ctx_t *ctx, ngx_str_t *path); +static ngx_int_t ngx_http_cache_purge_refresh_load_dir( + ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx, + ngx_str_t *path); static ngx_int_t ngx_http_cache_purge_add_variable(ngx_conf_t *cf); static ngx_int_t ngx_http_cache_purge_refresh_bypass_variable( ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); @@ -3573,19 +3589,6 @@ ngx_http_cache_purge_add_variable(ngx_conf_t *cf) } -/* - * Tree walk callback: collect matching cache files for refresh. - * Reads each cache file's binary header to extract ETag and Last-Modified, - * and extracts the URI from the stored cache key. - */ -static ngx_int_t -ngx_http_cache_purge_refresh_collect_file(ngx_tree_ctx_t *ctx, - ngx_str_t *path) -{ - return ngx_http_cache_purge_refresh_collect_path(ctx->data, path, 0); -} - - static ngx_int_t ngx_http_cache_purge_refresh_collect_open_file(ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx) @@ -3633,6 +3636,7 @@ ngx_http_cache_purge_refresh_collect_path( ssize_t n; size_t key_read_len; u_char *p, *q; + ngx_pool_t *pool; ngx_memzero(&path_copy, sizeof(ngx_str_t)); ngx_memzero(&uri, sizeof(ngx_str_t)); @@ -3641,6 +3645,8 @@ ngx_http_cache_purge_refresh_collect_path( ngx_memzero(&cache_key, sizeof(ngx_str_t)); ngx_memzero(&item, sizeof(ngx_http_cache_purge_invalidate_item_t)); + pool = rctx->request->pool; + if (rctx->timeout_enabled && !rctx->timed_out && ngx_current_msec >= rctx->deadline) { @@ -3735,7 +3741,7 @@ ngx_http_cache_purge_refresh_collect_path( return NGX_OK; } - path_data = ngx_pnalloc(rctx->request->pool, path->len + 1); + path_data = ngx_pnalloc(pool, path->len + 1); if (path_data == NULL) { ngx_free(key_buf); return NGX_ERROR; @@ -3753,7 +3759,7 @@ ngx_http_cache_purge_refresh_collect_path( q = (u_char *) ngx_strchr(p, '?'); if (q != NULL) { uri.len = q - p; - uri_data = ngx_pnalloc(rctx->request->pool, uri.len + 1); + uri_data = ngx_pnalloc(pool, uri.len + 1); if (uri_data == NULL) { ngx_free(key_buf); return NGX_ERROR; @@ -3764,7 +3770,7 @@ ngx_http_cache_purge_refresh_collect_path( q++; /* skip '?' */ args.len = n - rctx->key_prefix_len - uri.len - 1; - args_data = ngx_pnalloc(rctx->request->pool, args.len + 1); + args_data = ngx_pnalloc(pool, args.len + 1); if (args_data == NULL) { ngx_free(key_buf); return NGX_ERROR; @@ -3774,7 +3780,7 @@ ngx_http_cache_purge_refresh_collect_path( args.data = args_data; } else { uri.len = n - rctx->key_prefix_len; - uri_data = ngx_pnalloc(rctx->request->pool, uri.len + 1); + uri_data = ngx_pnalloc(pool, uri.len + 1); if (uri_data == NULL) { ngx_free(key_buf); return NGX_ERROR; @@ -3786,7 +3792,7 @@ ngx_http_cache_purge_refresh_collect_path( /* Store ETag from binary header */ if (header.etag_len > 0 && header.etag_len < NGX_HTTP_CACHE_ETAG_LEN) { - etag_data = ngx_pnalloc(rctx->request->pool, header.etag_len + 1); + etag_data = ngx_pnalloc(pool, header.etag_len + 1); if (etag_data == NULL) { ngx_free(key_buf); return NGX_ERROR; @@ -3806,7 +3812,7 @@ ngx_http_cache_purge_refresh_collect_path( item.last_modified = header.last_modified; item.fs_size = ngx_file_size(&fi); - cache_key_data = ngx_pnalloc(rctx->request->pool, n + 1); + cache_key_data = ngx_pnalloc(pool, n + 1); if (cache_key_data == NULL) { ngx_free(key_buf); return NGX_ERROR; @@ -4275,12 +4281,161 @@ ngx_http_cache_purge_refresh_finalize(ngx_http_request_t *r, } +static ngx_int_t +ngx_http_cache_purge_refresh_enqueue_dir( + ngx_http_cache_purge_refresh_ctx_t *ctx, ngx_str_t *path) +{ + ngx_http_cache_purge_refresh_dir_t *dir; + u_char *p; + + dir = ngx_palloc(ctx->request->pool, + sizeof(ngx_http_cache_purge_refresh_dir_t)); + if (dir == NULL) { + return NGX_ERROR; + } + + p = ngx_pnalloc(ctx->request->pool, path->len + 1); + if (p == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(p, path->data, path->len); + p[path->len] = '\0'; + + dir->path.len = path->len; + dir->path.data = p; + + ngx_queue_insert_tail(&ctx->pending_dirs, &dir->queue); + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_cache_purge_refresh_load_dir(ngx_http_request_t *r, + ngx_http_cache_purge_refresh_ctx_t *ctx, ngx_str_t *path) +{ + ngx_dir_t dir; + ngx_http_cache_purge_refresh_scan_entry_t *entry; + ngx_str_t child; + u_char *name; + u_char *p; + size_t len; + ngx_int_t rc; + + if (ctx->scan_pool != NULL) { + ngx_destroy_pool(ctx->scan_pool); + ctx->scan_pool = NULL; + } + + ctx->scan_pool = ngx_create_pool(4096, r->connection->log); + if (ctx->scan_pool == NULL) { + return NGX_ERROR; + } + + ctx->scan_entries = ngx_array_create(ctx->scan_pool, 64, + sizeof(ngx_http_cache_purge_refresh_scan_entry_t)); + if (ctx->scan_entries == NULL) { + return NGX_ERROR; + } + + ctx->scan_index = 0; + + if (ngx_open_dir(path, &dir) == NGX_ERROR) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno, + ngx_open_dir_n " \"%V\" failed", path); + return NGX_ERROR; + } + + rc = NGX_OK; + + for ( ;; ) { + ngx_set_errno(0); + + if (ngx_read_dir(&dir) == NGX_ERROR) { + if (ngx_errno != NGX_ENOMOREFILES) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno, + ngx_read_dir_n " \"%V\" failed", path); + rc = NGX_ERROR; + } + + break; + } + + len = ngx_de_namelen(&dir); + name = ngx_de_name(&dir); + + if (len == 1 && name[0] == '.') { + continue; + } + + if (len == 2 && name[0] == '.' && name[1] == '.') { + continue; + } + + if (path->len == ctx->cache->path->name.len + && ngx_strncmp(path->data, ctx->cache->path->name.data, + path->len) == 0 + && ngx_de_is_dir(&dir)) + { + if (len == sizeof("proxy_temp") - 1 + && ngx_strncmp(name, (u_char *) "proxy_temp", len) == 0) + { + continue; + } + } + + child.len = path->len + 1 + len; + child.data = ngx_pnalloc(ctx->scan_pool, child.len + 1); + if (child.data == NULL) { + rc = NGX_ERROR; + break; + } + + p = ngx_cpymem(child.data, path->data, path->len); + *p++ = '/'; + ngx_memcpy(p, name, len); + p[len] = '\0'; + + if (!dir.valid_info && ngx_de_info(child.data, &dir) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno, + ngx_de_info_n " \"%V\" failed", &child); + continue; + } + + if (!ngx_de_is_file(&dir) && !ngx_de_is_dir(&dir)) { + continue; + } + + entry = ngx_array_push(ctx->scan_entries); + if (entry == NULL) { + rc = NGX_ERROR; + break; + } + + entry->path = child; + entry->is_dir = ngx_de_is_dir(&dir); + } + + if (ngx_close_dir(&dir) == NGX_ERROR) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno, + ngx_close_dir_n " \"%V\" failed", path); + if (rc == NGX_OK) { + rc = NGX_ERROR; + } + } + + return rc; +} + + static ngx_int_t ngx_http_cache_purge_refresh_scan_next_chunk(ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx) { - ngx_tree_ctx_t tree; - ngx_int_t rc; + ngx_http_cache_purge_refresh_dir_t *dir; + ngx_http_cache_purge_refresh_scan_entry_t *entry; + ngx_int_t rc; if (ctx->scan_done) { return NGX_OK; @@ -4295,7 +4450,7 @@ ngx_http_cache_purge_refresh_scan_next_chunk(ngx_http_request_t *r, return NGX_ERROR; } - ctx->files = ngx_array_create(ctx->chunk_pool, ctx->chunk_limit, + ctx->files = ngx_array_create(r->pool, ctx->chunk_limit, sizeof(ngx_http_cache_purge_refresh_file_t)); if (ctx->files == NULL) { return NGX_ERROR; @@ -4303,33 +4458,89 @@ ngx_http_cache_purge_refresh_scan_next_chunk(ngx_http_request_t *r, ctx->current = 0; ctx->queued = 0; - ctx->scan_after = ctx->resume_path; if (ctx->exact) { ctx->scan_done = 1; return ngx_http_cache_purge_refresh_collect_open_file(r, ctx); } - tree.init_handler = NULL; - tree.file_handler = ngx_http_cache_purge_refresh_collect_file; - tree.pre_tree_handler = ngx_http_purge_file_cache_noop; - tree.post_tree_handler = ngx_http_purge_file_cache_noop; - tree.spec_handler = ngx_http_purge_file_cache_noop; - tree.data = ctx; - tree.alloc = 0; - tree.log = ngx_cycle->log; + if (!ctx->scan_initialized) { + ngx_queue_init(&ctx->pending_dirs); + if (ngx_http_cache_purge_refresh_enqueue_dir(ctx, + &ctx->cache->path->name) + != NGX_OK) + { + return NGX_ERROR; + } - rc = ngx_walk_tree(&tree, &ctx->cache->path->name); - if (rc == NGX_ABORT) { - return NGX_OK; + ctx->scan_initialized = 1; } - if (rc != NGX_OK) { - return rc; - } + for ( ;; ) { + if (ctx->timeout_enabled && !ctx->timed_out + && ngx_current_msec >= ctx->deadline) + { + ngx_http_cache_purge_refresh_mark_timeout(ctx); + ctx->scan_done = 1; + return NGX_OK; + } - ctx->scan_done = 1; - return NGX_OK; + if (ctx->queued >= ctx->chunk_limit) { + return NGX_OK; + } + + if (ctx->scan_entries != NULL + && ctx->scan_index < ctx->scan_entries->nelts) + { + entry = ((ngx_http_cache_purge_refresh_scan_entry_t *) + ctx->scan_entries->elts) + ctx->scan_index++; + + if (entry->is_dir) { + if (ngx_http_cache_purge_refresh_enqueue_dir(ctx, &entry->path) + != NGX_OK) + { + return NGX_ERROR; + } + + continue; + } + + rc = ngx_http_cache_purge_refresh_collect_path(ctx, &entry->path, 0); + if (rc != NGX_OK) { + if (rc == NGX_ABORT && ctx->timed_out) { + ctx->scan_done = 1; + return NGX_OK; + } + + return rc; + } + + continue; + } + + if (ctx->scan_pool != NULL) { + ngx_destroy_pool(ctx->scan_pool); + ctx->scan_pool = NULL; + } + + ctx->scan_entries = NULL; + ctx->scan_index = 0; + + if (ngx_queue_empty(&ctx->pending_dirs)) { + ctx->scan_done = 1; + return NGX_OK; + } + + dir = (ngx_http_cache_purge_refresh_dir_t *) ngx_queue_data( + ngx_queue_head(&ctx->pending_dirs), + ngx_http_cache_purge_refresh_dir_t, queue); + ngx_queue_remove(&dir->queue); + + rc = ngx_http_cache_purge_refresh_load_dir(r, ctx, &dir->path); + if (rc != NGX_OK) { + return rc; + } + } } @@ -4388,10 +4599,33 @@ ngx_http_cache_purge_refresh_start(ngx_http_request_t *r) } if (ctx->queued == 0) { - if (ctx->scan_done) { - ngx_http_cache_purge_refresh_finalize(r, ctx); + if (!ctx->scan_done) { + rc = ngx_http_cache_purge_refresh_scan_next_chunk(r, ctx); + if (rc != NGX_OK) { + ctx->errors++; + ngx_http_cache_purge_refresh_finalize(r, ctx); + return; + } + } + + if (ctx->timed_out) { + if (ctx->queued > 0) { + ctx->errors += ctx->queued; + ctx->current = ctx->queued; + } + + if (ctx->active == 0) { + ngx_http_cache_purge_refresh_finalize(r, ctx); + } + return; + } + + if (ctx->queued == 0) { + if (ctx->scan_done) { + ngx_http_cache_purge_refresh_finalize(r, ctx); + } + return; } - return; } cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); @@ -4431,11 +4665,9 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, { ngx_http_cache_purge_refresh_ctx_t *ctx; ngx_http_cache_purge_loc_conf_t *cplcf; - ngx_tree_ctx_t tree; ngx_str_t *keys; ngx_str_t key; ngx_str_t tail; - ngx_int_t rc; ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "cache purge refresh in %s", cache->path->name.data); @@ -4454,6 +4686,13 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, ctx->exact = !ctx->purge_all && !ngx_http_cache_purge_is_partial(r); ctx->timeout_enabled = (cplcf->refresh_timeout != 0); ctx->deadline = ngx_current_msec + cplcf->refresh_timeout; + ctx->chunk_limit = cplcf->refresh_concurrency * 4; + if (ctx->chunk_limit == 0) { + ctx->chunk_limit = 4; + } + if (ctx->chunk_limit < 32) { + ctx->chunk_limit = 32; + } ngx_memzero(&ctx->timeout_ev, sizeof(ngx_event_t)); ctx->timeout_ev.handler = ngx_http_cache_purge_refresh_timeout_handler; @@ -4508,48 +4747,6 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, } } - ctx->files = ngx_array_create(r->pool, 256, - sizeof(ngx_http_cache_purge_refresh_file_t)); - if (ctx->files == NULL) { - if (ctx->timeout_ev.timer_set) { - ngx_del_timer(&ctx->timeout_ev); - } - return NGX_HTTP_INTERNAL_SERVER_ERROR; - } - - if (ctx->exact) { - rc = ngx_http_cache_purge_refresh_collect_open_file(r, ctx); - if (rc == NGX_ABORT && ctx->timed_out) { - ctx->scan_done = 1; - } else if (rc != NGX_OK) { - if (ctx->timeout_ev.timer_set) { - ngx_del_timer(&ctx->timeout_ev); - } - return NGX_HTTP_INTERNAL_SERVER_ERROR; - } - } else { - tree.init_handler = NULL; - tree.file_handler = ngx_http_cache_purge_refresh_collect_file; - tree.pre_tree_handler = ngx_http_purge_file_cache_noop; - tree.post_tree_handler = ngx_http_purge_file_cache_noop; - tree.spec_handler = ngx_http_purge_file_cache_noop; - tree.data = ctx; - tree.alloc = 0; - tree.log = ngx_cycle->log; - - rc = ngx_walk_tree(&tree, &ctx->cache->path->name); - if (rc == NGX_ABORT && ctx->timed_out) { - ctx->scan_done = 1; - } else if (rc != NGX_OK) { - if (ctx->timeout_ev.timer_set) { - ngx_del_timer(&ctx->timeout_ev); - } - return NGX_HTTP_INTERNAL_SERVER_ERROR; - } - } - - ctx->scan_done = 1; - /* Set module context on the request */ ngx_http_set_ctx(r, ctx, ngx_http_cache_purge_module); From d736d284777631c1fe63293200040d010b61c0e0 Mon Sep 17 00:00:00 2001 From: yaoge123 Date: Thu, 19 Mar 2026 07:24:29 +0800 Subject: [PATCH 04/43] Harden refresh race safety and defensive checks - Compare invalidation metadata directly against in-memory cache struct instead of re-reading the cache file, eliminating a TOCTOU race window - Add inode (uniq) and body_start cross-checks for race detection - Defer temp pool destruction via enqueue/drain pattern to prevent use-after-free when subrequests reference pool-allocated data - Register pool cleanup handler to guarantee resource release on request termination (timeout, client disconnect) - Retire chunk pools instead of immediate destruction so in-flight subrequests retain valid file references across chunk boundaries - Add active count underflow guard in refresh_done normal path - Add args.len underflow guard for malformed cache keys - Use case-sensitive ngx_strncmp for cache key matching (keys are exact byte sequences, not case-insensitive identifiers) - Add bounds checks for config parsing (from keyword position) - Fix memory leak: free key_buf on short read in collect_path - Track dispatched subrequest count separately from queue cursor for accurate timeout skip accounting --- ngx_cache_purge_module.c | 514 ++++++++++++++++++++++++++++++--------- 1 file changed, 393 insertions(+), 121 deletions(-) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index 53bd83a..6954072 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -317,7 +317,9 @@ typedef enum { typedef struct { ngx_str_t cache_key; ngx_str_t cache_path; + ngx_file_uniq_t uniq; time_t last_modified; + u_short body_start; u_char etag_len; u_char etag[NGX_HTTP_CACHE_ETAG_LEN]; off_t fs_size; @@ -329,11 +331,15 @@ typedef struct { ngx_str_t partial_prefix; ngx_flag_t match_all; ngx_uint_t files_deleted; + ngx_queue_t temp_pools; } ngx_http_cache_purge_batch_ctx_t; static ngx_int_t ngx_http_cache_purge_read_item(ngx_pool_t *pool, ngx_log_t *log, ngx_str_t *path, ngx_http_cache_purge_invalidate_item_t *item); +static ngx_int_t ngx_http_cache_purge_enqueue_temp_pool(ngx_queue_t *queue, + ngx_pool_t *owner_pool, ngx_pool_t *pool); +static void ngx_http_cache_purge_drain_temp_pools(ngx_queue_t *queue); static ngx_int_t ngx_http_cache_purge_invalidate_opened_cache(ngx_log_t *log, ngx_http_file_cache_t *cache, ngx_http_cache_t *c, ngx_pool_t *pool, ngx_http_cache_purge_invalidate_item_t *item, @@ -341,9 +347,11 @@ static ngx_int_t ngx_http_cache_purge_invalidate_opened_cache(ngx_log_t *log, static ngx_int_t ngx_http_cache_purge_open_temp_cache(ngx_http_request_t *r, ngx_http_file_cache_t *cache, ngx_pool_t *pool, ngx_str_t *cache_key, ngx_http_cache_t *c); -static ngx_int_t ngx_http_cache_purge_item_metadata_matches( +static ngx_int_t ngx_http_cache_purge_item_matches_cache( ngx_http_cache_purge_invalidate_item_t *expected, - ngx_http_cache_purge_invalidate_item_t *current); + ngx_http_cache_t *c); +static ngx_int_t ngx_http_cache_purge_cache_matches_node( + ngx_http_cache_t *c); static ngx_int_t ngx_http_cache_purge_invalidate_item(ngx_http_request_t *r, ngx_http_file_cache_t *cache, ngx_pool_t *pool, ngx_http_cache_purge_invalidate_item_t *item, @@ -363,6 +371,11 @@ typedef struct { ngx_flag_t is_dir; } ngx_http_cache_purge_refresh_scan_entry_t; +typedef struct { + ngx_queue_t queue; + ngx_pool_t *pool; +} ngx_http_cache_purge_refresh_temp_pool_t; + typedef struct { ngx_queue_t queue; ngx_str_t path; @@ -383,18 +396,21 @@ typedef struct { /* collected refresh candidates */ ngx_pool_t *chunk_pool; + ngx_pool_t *retired_chunk_pool; + ngx_queue_t retired_chunk_pools; ngx_pool_t *scan_pool; ngx_array_t *files; /* collected files: ngx_http_cache_purge_refresh_file_t[] */ ngx_array_t *scan_entries; /* current directory entries */ ngx_uint_t current; /* next file index to dispatch */ ngx_uint_t queued; /* total collected file count */ ngx_uint_t active; /* active subrequest count */ + ngx_uint_t dispatched; /* total dispatched subrequests */ ngx_uint_t chunk_limit; ngx_uint_t scan_index; ngx_flag_t scan_done; /* collection complete */ ngx_flag_t scan_initialized; + ngx_queue_t temp_pools; ngx_queue_t pending_dirs; - /* stats */ ngx_uint_t total; ngx_uint_t refreshed; @@ -406,6 +422,7 @@ typedef struct { ngx_http_cache_purge_refresh_ctx_t *ctx; ngx_http_cache_purge_refresh_file_t *file; ngx_flag_t validation_ready; + ngx_flag_t handled; } ngx_http_cache_purge_refresh_post_data_t; static ngx_int_t ngx_http_cache_purge_refresh(ngx_http_request_t *r, @@ -420,11 +437,16 @@ static ngx_int_t ngx_http_cache_purge_refresh_fire_subrequest( ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); static ngx_int_t ngx_http_cache_purge_refresh_done( ngx_http_request_t *r, void *data, ngx_int_t rc); +static ngx_int_t ngx_http_cache_purge_refresh_enqueue_retired_chunk_pool( + ngx_http_cache_purge_refresh_ctx_t *ctx, ngx_pool_t *pool); +static void ngx_http_cache_purge_refresh_drain_retired_chunk_pools( + ngx_http_cache_purge_refresh_ctx_t *ctx); static ngx_int_t ngx_http_cache_purge_refresh_send_response( ngx_http_request_t *r); static void ngx_http_cache_purge_refresh_timeout_handler(ngx_event_t *ev); static void ngx_http_cache_purge_refresh_mark_timeout( ngx_http_cache_purge_refresh_ctx_t *ctx); +static void ngx_http_cache_purge_refresh_pool_cleanup(void *data); static void ngx_http_cache_purge_refresh_finalize( ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx); static ngx_int_t ngx_http_cache_purge_refresh_scan_next_chunk( @@ -2558,7 +2580,12 @@ ngx_http_cache_purge_invalidate_file(ngx_tree_ctx_t *ctx, ngx_str_t *path) } } - ngx_destroy_pool(pool); + if (ngx_http_cache_purge_enqueue_temp_pool(&data->temp_pools, + data->request->pool, pool) + != NGX_OK) + { + ngx_destroy_pool(pool); + } return NGX_OK; } @@ -2582,9 +2609,9 @@ ngx_http_cache_purge_invalidate_partial_file(ngx_tree_ctx_t *ctx, if (ngx_http_cache_purge_read_item(pool, ctx->log, path, &item) == NGX_OK) { if (data->partial_prefix.len == 0 || (item.cache_key.len >= data->partial_prefix.len - && ngx_strncasecmp(item.cache_key.data, - data->partial_prefix.data, - data->partial_prefix.len) == 0)) + && ngx_strncmp(item.cache_key.data, + data->partial_prefix.data, + data->partial_prefix.len) == 0)) { if (ngx_http_cache_purge_invalidate_item(data->request, data->cache, pool, &item, &result) @@ -2599,7 +2626,142 @@ ngx_http_cache_purge_invalidate_partial_file(ngx_tree_ctx_t *ctx, } } - ngx_destroy_pool(pool); + if (ngx_http_cache_purge_enqueue_temp_pool(&data->temp_pools, + data->request->pool, pool) + != NGX_OK) + { + ngx_destroy_pool(pool); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_cache_purge_enqueue_temp_pool(ngx_queue_t *queue, + ngx_pool_t *owner_pool, ngx_pool_t *pool) +{ + ngx_http_cache_purge_refresh_temp_pool_t *entry; + + if (pool == NULL) { + return NGX_OK; + } + + entry = ngx_palloc(owner_pool, + sizeof(ngx_http_cache_purge_refresh_temp_pool_t)); + if (entry == NULL) { + return NGX_ERROR; + } + + entry->pool = pool; + ngx_queue_insert_tail(queue, &entry->queue); + + return NGX_OK; +} + + +static void +ngx_http_cache_purge_drain_temp_pools(ngx_queue_t *queue) +{ + ngx_queue_t *q; + ngx_http_cache_purge_refresh_temp_pool_t *entry; + + while (!ngx_queue_empty(queue)) { + q = ngx_queue_head(queue); + entry = (ngx_http_cache_purge_refresh_temp_pool_t *) ngx_queue_data( + q, ngx_http_cache_purge_refresh_temp_pool_t, queue); + ngx_queue_remove(q); + + if (entry->pool != NULL) { + ngx_destroy_pool(entry->pool); + } + } +} + + +static ngx_int_t +ngx_http_cache_purge_read_item(ngx_pool_t *pool, ngx_log_t *log, + ngx_str_t *path, ngx_http_cache_purge_invalidate_item_t *item) +{ + ngx_http_file_cache_header_t header; + ngx_file_t file; + ngx_file_info_t fi; + u_char *key_buf; + size_t key_len; + ssize_t n; + + ngx_memzero(item, sizeof(ngx_http_cache_purge_invalidate_item_t)); + ngx_memzero(&file, sizeof(ngx_file_t)); + + file.fd = ngx_open_file(path->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, + NGX_FILE_DEFAULT_ACCESS); + if (file.fd == NGX_INVALID_FILE) { + return NGX_ERROR; + } + + file.log = log; + + if (ngx_fd_info(file.fd, &fi) == NGX_FILE_ERROR) { + ngx_close_file(file.fd); + return NGX_ERROR; + } + + item->fs_size = ngx_file_size(&fi); + item->uniq = ngx_file_uniq(&fi); + + n = ngx_read_file(&file, (u_char *) &header, sizeof(header), 0); + if (n < (ssize_t) sizeof(header)) { + ngx_close_file(file.fd); + return NGX_ERROR; + } + + if (header.header_start <= sizeof(header) + 6) { + ngx_close_file(file.fd); + return NGX_ERROR; + } + + key_len = header.header_start - sizeof(header) - 6; + if (key_len == 0 || key_len > 8192) { + ngx_close_file(file.fd); + return NGX_ERROR; + } + + key_buf = ngx_pnalloc(pool, key_len + 1); + if (key_buf == NULL) { + ngx_close_file(file.fd); + return NGX_ERROR; + } + + n = ngx_read_file(&file, key_buf, key_len, sizeof(header) + 6); + ngx_close_file(file.fd); + if (n < 1) { + return NGX_ERROR; + } + + key_buf[n] = '\0'; + if (n > 0 && key_buf[n - 1] == LF) { + key_buf[n - 1] = '\0'; + n--; + } + + item->cache_key.data = key_buf; + item->cache_key.len = n; + + item->cache_path.data = ngx_pnalloc(pool, path->len + 1); + if (item->cache_path.data == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(item->cache_path.data, path->data, path->len); + item->cache_path.data[path->len] = '\0'; + item->cache_path.len = path->len; + + item->last_modified = header.last_modified; + item->body_start = header.body_start; + if (header.etag_len > 0 && header.etag_len < NGX_HTTP_CACHE_ETAG_LEN) { + item->etag_len = header.etag_len; + ngx_memcpy(item->etag, header.etag, header.etag_len); + } return NGX_OK; } @@ -2611,7 +2773,7 @@ ngx_http_cache_purge_invalidate_opened_cache(ngx_log_t *log, ngx_pool_t *pool, ngx_http_cache_purge_invalidate_item_t *item, ngx_http_cache_purge_invalidate_result_e *result) { - ngx_http_cache_purge_invalidate_item_t current_item; + (void) pool; ngx_shmtx_lock(&cache->shpool->mutex); @@ -2622,16 +2784,9 @@ ngx_http_cache_purge_invalidate_opened_cache(ngx_log_t *log, } if (item != NULL) { - if (ngx_http_cache_purge_read_item(pool, log, &c->file.name, - ¤t_item) - != NGX_OK) + if (!ngx_http_cache_purge_item_matches_cache(item, c) + || !ngx_http_cache_purge_cache_matches_node(c)) { - ngx_shmtx_unlock(&cache->shpool->mutex); - *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING; - return NGX_OK; - } - - if (!ngx_http_cache_purge_item_metadata_matches(item, ¤t_item)) { ngx_shmtx_unlock(&cache->shpool->mutex); *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_REPLACED; return NGX_OK; @@ -2713,39 +2868,46 @@ ngx_http_cache_purge_open_temp_cache(ngx_http_request_t *r, static ngx_int_t -ngx_http_cache_purge_item_metadata_matches( +ngx_http_cache_purge_item_matches_cache( ngx_http_cache_purge_invalidate_item_t *expected, - ngx_http_cache_purge_invalidate_item_t *current) + ngx_http_cache_t *c) { - ngx_uint_t matched = 0; + ngx_uint_t matched; - if (expected->cache_key.len != current->cache_key.len - || ngx_strncmp(expected->cache_key.data, current->cache_key.data, - expected->cache_key.len) != 0) - { - return 0; - } + matched = 0; if (expected->etag_len > 0) { - if (expected->etag_len != current->etag_len - || ngx_memcmp(expected->etag, current->etag, expected->etag_len) != 0) + if (expected->etag_len != c->etag.len + || c->etag.data == NULL + || ngx_memcmp(expected->etag, c->etag.data, expected->etag_len) != 0) { return 0; } + matched = 1; } if (expected->last_modified > 0) { - if (expected->last_modified != current->last_modified) { + if (expected->last_modified != c->last_modified) { + return 0; + } + + matched = 1; + } + + if (expected->uniq != 0) { + if (expected->uniq != c->uniq) { return 0; } + matched = 1; } - if (expected->fs_size > 0) { - if (expected->fs_size != current->fs_size) { + if (expected->body_start > 0) { + if (expected->body_start != c->body_start) { return 0; } + matched = 1; } @@ -2753,6 +2915,31 @@ ngx_http_cache_purge_item_metadata_matches( } +static ngx_int_t +ngx_http_cache_purge_cache_matches_node(ngx_http_cache_t *c) +{ + if (!c->node->exists) { + return 0; + } + + if (c->uniq != 0 && c->node->uniq != c->uniq) { + return 0; + } + +# if (nginx_version >= 1000001) + if (c->node->fs_size != c->fs_size) { + return 0; + } +# endif + + if (c->node->body_start != c->body_start) { + return 0; + } + + return 1; +} + + static ngx_int_t ngx_http_cache_purge_invalidate_item(ngx_http_request_t *r, ngx_http_file_cache_t *cache, ngx_pool_t *pool, @@ -2775,7 +2962,7 @@ ngx_http_cache_purge_invalidate_item(ngx_http_request_t *r, } return ngx_http_cache_purge_invalidate_opened_cache(r->connection->log, - cache, &c, pool, item, + cache, &c, NULL, item, result); } @@ -3201,6 +3388,7 @@ ngx_http_cache_purge_all(ngx_http_request_t *r, ngx_http_file_cache_t *cache) ctx.cache = cache; ctx.match_all = 1; ctx.files_deleted = 0; + ngx_queue_init(&ctx.temp_pools); tree.file_handler = ngx_http_cache_purge_invalidate_file; tree.pre_tree_handler = ngx_http_purge_file_cache_noop; @@ -3210,6 +3398,7 @@ ngx_http_cache_purge_all(ngx_http_request_t *r, ngx_http_file_cache_t *cache) tree.log = ngx_cycle->log; ngx_walk_tree(&tree, &cache->path->name); + ngx_http_cache_purge_drain_temp_pools(&ctx.temp_pools); } ngx_uint_t @@ -3236,6 +3425,8 @@ ngx_http_cache_purge_partial(ngx_http_request_t *r, ctx.partial_prefix.data = key[0].data; ctx.partial_prefix.len = len; ctx.files_deleted = 0; + ngx_queue_init(&ctx.temp_pools); + ctx.match_all = (len == 0); tree.file_handler = ngx_http_cache_purge_invalidate_partial_file; tree.pre_tree_handler = ngx_http_purge_file_cache_noop; @@ -3245,6 +3436,8 @@ ngx_http_cache_purge_partial(ngx_http_request_t *r, tree.log = ngx_cycle->log; ngx_walk_tree(&tree, &cache->path->name); + return ctx.files_deleted; + ngx_http_cache_purge_drain_temp_pools(&ctx.temp_pools); return ctx.files_deleted; } @@ -3307,6 +3500,12 @@ ngx_http_cache_purge_conf(ngx_conf_t *cf, ngx_http_cache_purge_conf_t *cpcf) } + if (from_position >= cf->args->nelts) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "missing \"from\" keyword after optional parameters"); + return NGX_CONF_ERROR; + } + /* sanity check */ if (ngx_strcmp(value[from_position].data, "from") != 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, @@ -3315,6 +3514,12 @@ ngx_http_cache_purge_conf(ngx_conf_t *cf, ngx_http_cache_purge_conf_t *cpcf) return NGX_CONF_ERROR; } + if (from_position + 1 >= cf->args->nelts) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "missing argument after \"from\" keyword"); + return NGX_CONF_ERROR; + } + if (ngx_strcmp(value[from_position + 1].data, "all") == 0) { cpcf->enable = 1; return NGX_CONF_OK; @@ -3645,7 +3850,7 @@ ngx_http_cache_purge_refresh_collect_path( ngx_memzero(&cache_key, sizeof(ngx_str_t)); ngx_memzero(&item, sizeof(ngx_http_cache_purge_invalidate_item_t)); - pool = rctx->request->pool; + pool = rctx->chunk_pool != NULL ? rctx->chunk_pool : rctx->request->pool; if (rctx->timeout_enabled && !rctx->timed_out && ngx_current_msec >= rctx->deadline) @@ -3703,6 +3908,7 @@ ngx_http_cache_purge_refresh_collect_path( ngx_close_file(f.fd); if (n < 1) { + ngx_free(key_buf); return NGX_OK; } @@ -3715,8 +3921,8 @@ ngx_http_cache_purge_refresh_collect_path( if (exact_match) { if ((size_t) n != rctx->key_partial.len - || ngx_strncasecmp(key_buf, rctx->key_partial.data, - rctx->key_partial.len) != 0) + || ngx_strncmp(key_buf, rctx->key_partial.data, + rctx->key_partial.len) != 0) { ngx_free(key_buf); return NGX_OK; @@ -3728,8 +3934,8 @@ ngx_http_cache_purge_refresh_collect_path( ngx_free(key_buf); return NGX_OK; /* key too short to match */ } - if (ngx_strncasecmp(key_buf, rctx->key_partial.data, - rctx->key_partial.len) != 0) + if (ngx_strncmp(key_buf, rctx->key_partial.data, + rctx->key_partial.len) != 0) { ngx_free(key_buf); return NGX_OK; /* no match */ @@ -3769,6 +3975,10 @@ ngx_http_cache_purge_refresh_collect_path( uri.data = uri_data; q++; /* skip '?' */ + if ((size_t) n < rctx->key_prefix_len + uri.len + 1) { + ngx_free(key_buf); + return NGX_OK; /* malformed key, skip */ + } args.len = n - rctx->key_prefix_len - uri.len - 1; args_data = ngx_pnalloc(pool, args.len + 1); if (args_data == NULL) { @@ -3809,7 +4019,9 @@ ngx_http_cache_purge_refresh_collect_path( } /* Store Last-Modified */ + item.uniq = ngx_file_uniq(&fi); item.last_modified = header.last_modified; + item.body_start = header.body_start; item.fs_size = ngx_file_size(&fi); cache_key_data = ngx_pnalloc(pool, n + 1); @@ -3860,43 +4072,24 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, ngx_http_cache_purge_refresh_ctx_t *ctx; ngx_http_cache_purge_refresh_file_t *file; ngx_http_cache_purge_invalidate_result_e invalidate_result; - ngx_pool_t *pool; ngx_uint_t status; pd = data; ctx = pd->ctx; file = pd->file; + if (pd->handled) { + return NGX_OK; + } + + pd->handled = 1; + if (!pd->validation_ready) { ctx->errors++; if (ctx->active > 0) { ctx->active--; } - if (ctx->timed_out) { - if (ctx->active == 0) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - return NGX_OK; - } - - if (ctx->current < ctx->queued) { - ngx_int_t fire_rc; - - fire_rc = ngx_http_cache_purge_refresh_fire_subrequest( - ctx->request, ctx); - if (fire_rc == NGX_ABORT && ctx->timed_out) { - if (ctx->active == 0) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - } else if (fire_rc != NGX_OK) { - ctx->errors += ctx->queued - ctx->current; - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - } else if (ctx->active == 0) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - return NGX_OK; } @@ -3919,6 +4112,8 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, "refresh: 304 kept \"%V\"", &file->uri); } else if (status == NGX_HTTP_OK) { /* 200 — content changed, invalidate through unified helper */ + ngx_pool_t *pool; + pool = ngx_create_pool(4096, r->connection->log); if (pool == NULL) { ctx->errors++; @@ -3952,7 +4147,13 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, } if (pool != NULL) { - ngx_destroy_pool(pool); + if (ngx_http_cache_purge_enqueue_temp_pool(&ctx->temp_pools, + ctx->request->pool, pool) + != NGX_OK) + { + ngx_destroy_pool(pool); + ctx->errors++; + } } } else { /* Error or unexpected status — keep cache (conservative) */ @@ -3961,7 +4162,9 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, "refresh: %ui error kept \"%V\"", status, &file->uri); } - ctx->active--; + if (ctx->active > 0) { + ctx->active--; + } if (ctx->timed_out) { if (ctx->current < ctx->queued) { @@ -3969,55 +4172,9 @@ ngx_http_cache_purge_refresh_done(ngx_http_request_t *r, void *data, ctx->current = ctx->queued; } - if (ctx->active == 0) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - return NGX_OK; } - if (ctx->current < ctx->queued) { - ngx_int_t fire_rc; - - fire_rc = ngx_http_cache_purge_refresh_fire_subrequest(ctx->request, - ctx); - if (fire_rc == NGX_ABORT && ctx->timed_out) { - if (ctx->active == 0) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - } else if (fire_rc != NGX_OK) { - ctx->errors += ctx->queued - ctx->current; - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - } else if (!ctx->scan_done && ctx->active == 0) { - if (ngx_http_cache_purge_refresh_scan_next_chunk(ctx->request, ctx) - != NGX_OK) - { - ctx->errors++; - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - return NGX_OK; - } - - if (ctx->queued > 0) { - ngx_int_t fire_rc; - - fire_rc = ngx_http_cache_purge_refresh_fire_subrequest( - ctx->request, ctx); - if (fire_rc == NGX_ABORT && ctx->timed_out) { - if (ctx->active == 0) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - } else if (fire_rc != NGX_OK) { - ctx->errors += ctx->queued - ctx->current; - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - } else if (ctx->scan_done) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - } else if (ctx->active == 0) { - ngx_http_cache_purge_refresh_finalize(ctx->request, ctx); - } - return NGX_OK; } @@ -4065,6 +4222,7 @@ ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, pd->ctx = ctx; pd->file = file; pd->validation_ready = 0; + pd->handled = 0; ps->handler = ngx_http_cache_purge_refresh_done; ps->data = pd; @@ -4075,12 +4233,16 @@ ngx_http_cache_purge_refresh_fire_subrequest(ngx_http_request_t *r, &sr, ps, NGX_HTTP_SUBREQUEST_WAITED); if (rc != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "refresh fire subrequest failed rc=%i uri=\"%V\"", + rc, &file->uri); ctx->errors++; return rc; } ctx->current++; ctx->active++; + ctx->dispatched++; /* Change method to HEAD */ sr->method = NGX_HTTP_HEAD; @@ -4247,10 +4409,11 @@ ngx_http_cache_purge_refresh_mark_timeout(ngx_http_cache_purge_refresh_ctx_t *ct cplcf = ngx_http_get_module_loc_conf(ctx->request, ngx_http_cache_purge_module); - if (ctx->current < ctx->total) { - skipped = ctx->total - ctx->current; + if (ctx->dispatched < ctx->total) { + skipped = ctx->total - ctx->dispatched; ctx->errors += skipped; - ctx->current = ctx->total; + ctx->current = ctx->queued; + ctx->dispatched = ctx->total; } else { skipped = 0; @@ -4263,10 +4426,91 @@ ngx_http_cache_purge_refresh_mark_timeout(ngx_http_cache_purge_refresh_ctx_t *ct } +static void +ngx_http_cache_purge_refresh_pool_cleanup(void *data) +{ + ngx_http_cache_purge_refresh_ctx_t *ctx; + + ctx = data; + + if (ctx == NULL) { + return; + } + + ctx->finalized = 1; + + if (ctx->timeout_ev.timer_set) { + ngx_del_timer(&ctx->timeout_ev); + } + + if (ctx->scan_pool != NULL) { + ngx_destroy_pool(ctx->scan_pool); + ctx->scan_pool = NULL; + } + + ngx_http_cache_purge_drain_temp_pools(&ctx->temp_pools); + + ngx_http_cache_purge_refresh_drain_retired_chunk_pools(ctx); + + if (ctx->chunk_pool != NULL) { + ngx_destroy_pool(ctx->chunk_pool); + ctx->chunk_pool = NULL; + } + + if (ctx->retired_chunk_pool != NULL) { + ngx_destroy_pool(ctx->retired_chunk_pool); + ctx->retired_chunk_pool = NULL; + } +} + + +static ngx_int_t +ngx_http_cache_purge_refresh_enqueue_retired_chunk_pool( + ngx_http_cache_purge_refresh_ctx_t *ctx, ngx_pool_t *pool) +{ + ngx_http_cache_purge_refresh_temp_pool_t *entry; + + entry = ngx_palloc(ctx->request->pool, + sizeof(ngx_http_cache_purge_refresh_temp_pool_t)); + if (entry == NULL) { + return NGX_ERROR; + } + + entry->pool = pool; + ngx_queue_insert_tail(&ctx->retired_chunk_pools, &entry->queue); + + return NGX_OK; +} + + +static void +ngx_http_cache_purge_refresh_drain_retired_chunk_pools( + ngx_http_cache_purge_refresh_ctx_t *ctx) +{ + ngx_queue_t *q; + ngx_http_cache_purge_refresh_temp_pool_t *entry; + + while (!ngx_queue_empty(&ctx->retired_chunk_pools)) { + q = ngx_queue_head(&ctx->retired_chunk_pools); + ngx_queue_remove(q); + + entry = ngx_queue_data(q, + ngx_http_cache_purge_refresh_temp_pool_t, + queue); + + if (entry->pool != NULL) { + ngx_destroy_pool(entry->pool); + } + } +} + + static void ngx_http_cache_purge_refresh_finalize(ngx_http_request_t *r, ngx_http_cache_purge_refresh_ctx_t *ctx) { + ngx_int_t rc; + if (ctx->finalized) { return; } @@ -4277,7 +4521,8 @@ ngx_http_cache_purge_refresh_finalize(ngx_http_request_t *r, ngx_del_timer(&ctx->timeout_ev); } - ngx_http_finalize_request(r, ngx_http_cache_purge_refresh_send_response(r)); + rc = ngx_http_cache_purge_refresh_send_response(r); + ngx_http_finalize_request(r, rc); } @@ -4441,8 +4686,19 @@ ngx_http_cache_purge_refresh_scan_next_chunk(ngx_http_request_t *r, return NGX_OK; } + if (ctx->retired_chunk_pool != NULL) { + if (ngx_http_cache_purge_refresh_enqueue_retired_chunk_pool( + ctx, ctx->retired_chunk_pool) + != NGX_OK) + { + ngx_destroy_pool(ctx->retired_chunk_pool); + } + ctx->retired_chunk_pool = NULL; + } + if (ctx->chunk_pool != NULL) { - ngx_destroy_pool(ctx->chunk_pool); + ctx->retired_chunk_pool = ctx->chunk_pool; + ctx->chunk_pool = NULL; } ctx->chunk_pool = ngx_create_pool(4096, r->connection->log); @@ -4450,7 +4706,7 @@ ngx_http_cache_purge_refresh_scan_next_chunk(ngx_http_request_t *r, return NGX_ERROR; } - ctx->files = ngx_array_create(r->pool, ctx->chunk_limit, + ctx->files = ngx_array_create(ctx->chunk_pool, ctx->chunk_limit, sizeof(ngx_http_cache_purge_refresh_file_t)); if (ctx->files == NULL) { return NGX_ERROR; @@ -4598,6 +4854,11 @@ ngx_http_cache_purge_refresh_start(ngx_http_request_t *r) return; } + if (ctx->active == 0 && ctx->current >= ctx->queued && !ctx->scan_done) { + ctx->current = 0; + ctx->queued = 0; + } + if (ctx->queued == 0) { if (!ctx->scan_done) { rc = ngx_http_cache_purge_refresh_scan_next_chunk(r, ctx); @@ -4665,6 +4926,7 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, { ngx_http_cache_purge_refresh_ctx_t *ctx; ngx_http_cache_purge_loc_conf_t *cplcf; + ngx_pool_cleanup_t *cln; ngx_str_t *keys; ngx_str_t key; ngx_str_t tail; @@ -4678,9 +4940,19 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, return NGX_HTTP_INTERNAL_SERVER_ERROR; } + ngx_queue_init(&ctx->temp_pools); + ngx_queue_init(&ctx->retired_chunk_pools); + ctx->request = r; ctx->cache = cache; + cln = ngx_pool_cleanup_add(r->pool, 0); + if (cln == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + cln->handler = ngx_http_cache_purge_refresh_pool_cleanup; + cln->data = ctx; + cplcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_purge_module); ctx->purge_all = cplcf->conf->purge_all; ctx->exact = !ctx->purge_all && !ngx_http_cache_purge_is_partial(r); @@ -4725,8 +4997,8 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, } if (tail.len > 0 && key.len >= tail.len - && ngx_strncasecmp(key.data + key.len - tail.len, tail.data, - tail.len) == 0) + && ngx_strncmp(key.data + key.len - tail.len, tail.data, + tail.len) == 0) { ctx->key_prefix_len = key.len - tail.len; @@ -4737,8 +5009,8 @@ ngx_http_cache_purge_refresh(ngx_http_request_t *r, } if (tail.len > 0 && key.len >= tail.len - && ngx_strncasecmp(key.data + key.len - tail.len, tail.data, - tail.len) == 0) + && ngx_strncmp(key.data + key.len - tail.len, tail.data, + tail.len) == 0) { ctx->key_prefix_len = key.len - tail.len; From 6ad742f4ff4c93ec508bd622cbfad9c527756b05 Mon Sep 17 00:00:00 2001 From: yaoge123 Date: Thu, 19 Mar 2026 15:28:59 +0800 Subject: [PATCH 05/43] Fix concurrent refresh SIGSEGV caused by stale cache struct reference Root cause: ngx_http_file_cache_open registers a pool cleanup (ngx_http_file_cache_cleanup) that holds a pointer to the ngx_http_cache_t struct. When this struct was a stack variable in invalidate_item, the pointer became dangling after the function returned. Later, drain_temp_pools destroyed the pool, triggering the cleanup which accessed the stale stack address -> SIGSEGV. Fix: - Allocate ngx_http_cache_t on the pool instead of the stack so it survives until pool destruction - Call ngx_http_file_cache_free after invalidate_opened_cache to properly release the node reference (sets c->updated=1, making the pool cleanup a no-op) - Remove count-- from invalidate_opened_cache to avoid premature node free when concurrent refreshes race on the same cache entry Verified: 3-5 concurrent refresh requests to overlapping cache zones no longer crash. Full regression suite and 100k perf pass. --- ngx_cache_purge_module.c | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/ngx_cache_purge_module.c b/ngx_cache_purge_module.c index 6954072..8f96d25 100644 --- a/ngx_cache_purge_module.c +++ b/ngx_cache_purge_module.c @@ -2946,12 +2946,22 @@ ngx_http_cache_purge_invalidate_item(ngx_http_request_t *r, ngx_http_cache_purge_invalidate_item_t *item, ngx_http_cache_purge_invalidate_result_e *result) { - ngx_http_cache_t c; + ngx_http_cache_t *c; ngx_int_t rc; *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_ERROR; - rc = ngx_http_cache_purge_open_temp_cache(r, cache, pool, &item->cache_key, &c); + /* + * Allocate c on the pool rather than the stack. + * ngx_http_file_cache_open registers a pool cleanup that references c, + * so c must survive until the pool is destroyed. + */ + c = ngx_pcalloc(pool, sizeof(ngx_http_cache_t)); + if (c == NULL) { + return NGX_ERROR; + } + + rc = ngx_http_cache_purge_open_temp_cache(r, cache, pool, &item->cache_key, c); if (rc == NGX_DECLINED) { *result = NGX_HTTP_CACHE_PURGE_INVALIDATE_RACED_MISSING; return NGX_OK; @@ -2961,9 +2971,21 @@ ngx_http_cache_purge_invalidate_item(ngx_http_request_t *r, return NGX_ERROR; } - return ngx_http_cache_purge_invalidate_opened_cache(r->connection->log, - cache, &c, NULL, item, - result); + rc = ngx_http_cache_purge_invalidate_opened_cache(r->connection->log, + cache, c, NULL, item, + result); + + /* + * Release the node reference acquired by open_temp_cache. + * ngx_http_file_cache_free decrements count under mutex and, + * if exists==0 && count==0, safely removes the node from the + * rbtree and frees the slab memory. It also sets c->updated=1, + * so the pool cleanup (ngx_http_file_cache_cleanup) will be a + * no-op when the pool is eventually destroyed. + */ + ngx_http_file_cache_free(c, NULL); + + return rc; } ngx_int_t From eea295b1129c69113b9aa48924a6fbaf634b3d34 Mon Sep 17 00:00:00 2001 From: yaoge123 Date: Thu, 19 Mar 2026 21:15:07 +0800 Subject: [PATCH 06/43] Harden code quality and add refresh documentation - Add finalized guard in refresh_done to prevent use-after-free on timeout - Fix item_matches_cache to allow invalidate when all metadata fields are zero - Set pd->handled in fire_subrequest error paths to prevent double error counting - Set file.name in read_item and collect_path for proper error logging - Add ngx_memzero for batch_ctx in purge_all and purge_partial - Add detailed comment on open_temp_cache shallow copy risks - Add comprehensive refresh documentation to README.md --- README.md | 102 +++++++++++++++++++++++++++++++++++++++ ngx_cache_purge_module.c | 63 ++++++++++++++++++------ 2 files changed, 149 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8704744..0415555 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,108 @@ match will not align with the stored key. ## Sample configurations +## Refresh (conditional cache validation) + +Instead of blindly purging matched cache entries, `refresh` sends conditional +`HEAD` subrequests upstream using `If-None-Match` / `If-Modified-Since` +headers extracted from each cached file. Entries that return `200` are +purged; entries that return `304` are kept. + +Refresh currently supports `proxy_cache` only. Attempting refresh on +`fastcgi`, `scgi`, or `uwsgi` caches returns `400 Bad Request`. + +### Refresh directives + +`cache_purge_refresh` + +``` +Syntax: cache_purge_refresh on | off +Default: off +Context: http, server, location +``` + +Enables refresh mode so purge requests perform conditional validation instead +of unconditional deletion. + +`cache_purge_refresh_timeout` + +``` +Syntax: cache_purge_refresh_timeout