Skip to content
Open
14 changes: 10 additions & 4 deletions doc/admin-guide/files/records.yaml.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2892,7 +2892,7 @@ Dynamic Content & Content Negotiation
The number of times to attempt a cache open write upon failure to get a write lock.

This config is ignored when :ts:cv:`proxy.config.http.cache.open_write_fail_action` is
set to ``5`` or :ts:cv:`proxy.config.http.cache.max_open_write_retry_timeout` is set to gt ``0``.
set to ``5`` or ``6``, or when :ts:cv:`proxy.config.http.cache.max_open_write_retry_timeout` is set to gt ``0``.

.. ts:cv:: CONFIG proxy.config.http.cache.max_open_write_retry_timeout INT 0
:reloadable:
Expand All @@ -2901,7 +2901,7 @@ Dynamic Content & Content Negotiation
A timeout for attempting a cache open write upon failure to get a write lock.

This config is ignored when :ts:cv:`proxy.config.http.cache.open_write_fail_action` is
set to ``5``.
set to ``5`` or ``6``.

.. ts:cv:: CONFIG proxy.config.http.cache.open_write_fail_action INT 0
:reloadable:
Expand Down Expand Up @@ -2929,8 +2929,14 @@ Dynamic Content & Content Negotiation
with :ts:cv:`proxy.config.cache.enable_read_while_writer` configuration
allows to collapse concurrent requests without a need for any plugin.
Make sure to configure the :ref:`admin-config-read-while-writer` feature
correctly. Note that this option may result in CACHE_LOOKUP_COMPLETE HOOK
being called back more than once.
correctly. With this option, CACHE_LOOKUP_COMPLETE HOOK is deferred for
read retries so that plugins see only the final cache lookup result.
``6`` Retry Cache Read on a Cache Write Lock failure (same as ``5``), but if
read retries are exhausted and a stale cached object exists, serve the
stale content if allowed. This combines the request collapsing behavior
of ``5`` with the stale-serving fallback of ``2``. If stale is not
returnable (e.g., due to ``Cache-Control: must-revalidate``), go to
origin server.
===== ======================================================================

Customizable User Response Pages
Expand Down
1 change: 1 addition & 0 deletions include/proxy/http/HttpConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ enum class CacheOpenWriteFailAction_t {
ERROR_ON_MISS_STALE_ON_REVALIDATE = 0x03,
ERROR_ON_MISS_OR_REVALIDATE = 0x04,
READ_RETRY = 0x05,
READ_RETRY_STALE_ON_REVALIDATE = 0x06,
TOTAL_TYPES
};

Expand Down
16 changes: 15 additions & 1 deletion include/proxy/http/HttpTransact.h
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ class HttpTransact
HTTPInfo transform_store;
CacheDirectives directives;
HTTPInfo *object_read = nullptr;
HTTPInfo *stale_fallback = nullptr; // Saved stale object for action 6 fallback during retry
CacheWriteLock_t write_lock_state = CacheWriteLock_t::INIT;
int lookup_count = 0;
SquidHitMissCode hit_miss_code = SQUID_MISS_NONE;
Expand Down Expand Up @@ -703,6 +704,15 @@ class HttpTransact
/// configuration.
bool is_cacheable_due_to_negative_caching_configuration = false;

/// Set when stale content is served due to cache write lock failure.
/// Used to correctly attribute statistics and VIA strings.
bool serving_stale_due_to_write_lock = false;

/// Set when CACHE_LOOKUP_COMPLETE hook is deferred for action 5/6.
/// The hook will fire later with the final result once we know if
/// stale content will be served or if we're going to origin.
bool cache_lookup_complete_deferred = false;

MgmtByte cache_open_write_fail_action = 0;

HttpConfigParams *http_config_param = nullptr;
Expand Down Expand Up @@ -998,9 +1008,12 @@ class HttpTransact
static void HandleCacheOpenReadHitFreshness(State *s);
static void HandleCacheOpenReadHit(State *s);
static void HandleCacheOpenReadMiss(State *s);
static void HandleCacheOpenReadMissGoToOrigin(State *s);
static void set_cache_prepare_write_action_for_new_request(State *s);
static void build_response_from_cache(State *s, HTTPWarningCode warning_code);
static void handle_cache_write_lock(State *s);
static void handle_cache_write_lock_go_to_origin(State *s);
static void handle_cache_write_lock_go_to_origin_continue(State *s);
static void HandleResponse(State *s);
static void HandleUpdateCachedObject(State *s);
static void HandleUpdateCachedObjectContinue(State *s);
Expand Down Expand Up @@ -1093,7 +1106,8 @@ class HttpTransact
static void handle_response_keep_alive_headers(State *s, HTTPVersion ver, HTTPHdr *heads);
static int get_max_age(HTTPHdr *response);
static int calculate_document_freshness_limit(State *s, HTTPHdr *response, time_t response_date, bool *heuristic);
static Freshness_t what_is_document_freshness(State *s, HTTPHdr *client_request, HTTPHdr *cached_obj_response);
static Freshness_t what_is_document_freshness(State *s, HTTPHdr *client_request, HTTPHdr *cached_obj_response,
bool evaluate_actual_freshness = false);
static Authentication_t AuthenticationNeeded(const OverridableHttpConfigParams *p, HTTPHdr *client_request,
HTTPHdr *obj_response);
static void handle_parent_down(State *s);
Expand Down
18 changes: 12 additions & 6 deletions src/proxy/http/HttpCacheSM.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
namespace
{
DbgCtl dbg_ctl_http_cache{"http_cache"};

// Helper to check if cache_open_write_fail_action has READ_RETRY behavior
inline bool
is_read_retry_action(MgmtByte action)
{
return action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY) ||
action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY_STALE_ON_REVALIDATE);
}
} // end anonymous namespace

////
Expand Down Expand Up @@ -215,12 +223,11 @@ HttpCacheSM::state_cache_open_write(int event, void *data)
break;

case CACHE_EVENT_OPEN_WRITE_FAILED: {
if (master_sm->t_state.txn_conf->cache_open_write_fail_action ==
static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY)) {
if (is_read_retry_action(master_sm->t_state.txn_conf->cache_open_write_fail_action)) {
// fall back to open_read_tries
// Note that when CacheOpenWriteFailAction_t::READ_RETRY is configured, max_cache_open_write_retries
// Note that when READ_RETRY actions are configured, max_cache_open_write_retries
// is automatically ignored. Make sure to not disable max_cache_open_read_retries
// with CacheOpenWriteFailAction_t::READ_RETRY as this results in proxy'ing to origin
// with READ_RETRY actions as this results in proxy'ing to origin
// without write retries in both a cache miss or a cache refresh scenario.

if (write_retry_done()) {
Expand Down Expand Up @@ -264,8 +271,7 @@ HttpCacheSM::state_cache_open_write(int event, void *data)
_read_retry_event = nullptr;
}

if (master_sm->t_state.txn_conf->cache_open_write_fail_action ==
static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY)) {
if (is_read_retry_action(master_sm->t_state.txn_conf->cache_open_write_fail_action)) {
Dbg(dbg_ctl_http_cache,
"[%" PRId64 "] [state_cache_open_write] cache open write failure %d. "
"falling back to read retry...",
Expand Down
4 changes: 3 additions & 1 deletion src/proxy/http/HttpConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,9 @@ HttpConfig::reconfigure()
params->disallow_post_100_continue = INT_TO_BOOL(m_master.disallow_post_100_continue);

params->oride.cache_open_write_fail_action = m_master.oride.cache_open_write_fail_action;
if (params->oride.cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY)) {
if (params->oride.cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY) ||
params->oride.cache_open_write_fail_action ==
static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY_STALE_ON_REVALIDATE)) {
if (params->oride.max_cache_open_read_retries <= 0 || params->oride.max_cache_open_write_retries <= 0) {
Warning("Invalid config, cache_open_write_fail_action (%d), max_cache_open_read_retries (%" PRIu64 "), "
"max_cache_open_write_retries (%" PRIu64 ")",
Expand Down
29 changes: 23 additions & 6 deletions src/proxy/http/HttpSM.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2537,9 +2537,11 @@ HttpSM::state_cache_open_write(int event, void *data)
case CACHE_EVENT_OPEN_READ:
if (!t_state.cache_info.object_read) {
t_state.cache_open_write_fail_action = t_state.txn_conf->cache_open_write_fail_action;
// Note that CACHE_LOOKUP_COMPLETE may be invoked more than once
// if CacheOpenWriteFailAction_t::READ_RETRY is configured
ink_assert(t_state.cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY));
// READ_RETRY mode: write lock failed, no stale object available.
// CACHE_LOOKUP_COMPLETE will fire from HandleCacheOpenReadMiss with MISS result.
ink_assert(t_state.cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY) ||
t_state.cache_open_write_fail_action ==
static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY_STALE_ON_REVALIDATE));
t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::NONE;
t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::READ_RETRY;
break;
Expand All @@ -2558,8 +2560,9 @@ HttpSM::state_cache_open_write(int event, void *data)
t_state.source = HttpTransact::Source_t::CACHE;
// clear up CacheLookupResult_t::MISS, let Freshness function decide
// hit status
t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::NONE;
t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::READ_RETRY;
t_state.cache_open_write_fail_action = t_state.txn_conf->cache_open_write_fail_action;
t_state.cache_lookup_result = HttpTransact::CacheLookupResult_t::NONE;
t_state.cache_info.write_lock_state = HttpTransact::CacheWriteLock_t::READ_RETRY;
break;

case HTTP_TUNNEL_EVENT_DONE:
Expand Down Expand Up @@ -2663,7 +2666,21 @@ HttpSM::state_cache_open_read(int event, void *data)

ink_assert(t_state.transact_return_point == nullptr);
t_state.transact_return_point = HttpTransact::HandleCacheOpenRead;
setup_cache_lookup_complete_api();

// For READ_RETRY actions (5 and 6), skip the CACHE_LOOKUP_COMPLETE hook now.
// The hook will fire later with the final result: HIT if stale content is found
// during retry, or MISS if nothing is found (from HandleCacheOpenReadMiss).
// This ensures plugins see only the final cache lookup result, avoiding issues
// like stats double-counting and duplicate hook registrations.
if (t_state.txn_conf->cache_open_write_fail_action == static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY) ||
t_state.txn_conf->cache_open_write_fail_action ==
static_cast<MgmtByte>(CacheOpenWriteFailAction_t::READ_RETRY_STALE_ON_REVALIDATE)) {
SMDbg(dbg_ctl_http, "READ_RETRY configured, deferring CACHE_LOOKUP_COMPLETE hook");
t_state.cache_lookup_complete_deferred = true;
call_transact_and_set_next_state(nullptr);
} else {
setup_cache_lookup_complete_api();
}
break;

default:
Expand Down
Loading