Mark stale cache content as "invalid" on non-cacheable responses

Mindaugas Rasiukevicius rmind at noxt.eu
Wed Nov 18 22:47:18 UTC 2015


Maxim Dounin <mdounin at mdounin.ru> wrote:
> > 
> >    A cache MUST NOT generate a stale response if it is prohibited by an
> >    explicit in-protocol directive (e.g., by a "no-store" or "no-cache"
> >    cache directive, a "must-revalidate" cache-response-directive, or an
> >    applicable "s-maxage" or "proxy-revalidate" cache-response-directive;
> >    see Section 5.2.2).
> 
> The response stored in cache doesn't have "no-cache" nor any other 
> directives in it, and this "MUST NOT" certainly doesn't apply to 
> it.
> 
> In the scenario I've described, the response in cache is a correct 
> (but stale) response, as returned by an upstream server when it 
> was up and running normally.
> 
> In the scenario you've described, the response in cache is a 
> "temporary cacheable error", and it doesn't have any directives 
> attached to it either.

It does not, but the *subsequent* request does and the cache should obey
the most recent one.  I am not sure why are you focusing on the
per-request narrative when the cache is inherently about the state.  The
Cache-Control header is a way to control that state.  Again, RFC seems to
be fairly clear: the "cache MUST NOT reuse a stored response" (note the
word "reuse"), unless as described in the last bullet point of section 4,
the stored response is either:

      *  fresh (see Section 4.2), or
      *  allowed to be served stale (see Section 4.2.4), or
      *  successfully validated (see Section 4.3).

The race conditions we are talking about happen *after* the upstream
server advertises 'no-cache', therefore the second point is no longer
satisfied (and, of course, neither are the other two).  And further:

   When more than one suitable response is stored, a cache MUST use the
   most recent response (as determined by the Date header field).  It
   can also forward the request with "Cache-Control: max-age=0" or
   "Cache-Control: no-cache" to disambiguate which response to use.

> Use case 1, a cache with possible errors:
> 
> A high traffic resource, which normally can be cached for a long 
> time, but takes a long time to generate.  A response is stored in 
> the cache, and "proxy_cache_use_stale updating" is used to prevent 
> multiple clients from updating the cache at the same time.  If at 
> some point a request to update the cache fails / times out, an 
> "degraded" version is returned with caching disabled (this can be 
> an error, or normal response without some data).  The response 
> previously stored in the cache is preserved and will be returned 
> to other clients while we'll try to update the cache again.

The cache can still legitimately serve the content while update is in
progress and if the cache itself experienced a timeout while fetching
from the upstream server (because the state is still "unknown" for the
cache).  However, if the upstream server sent a response with 'no-cache',
then as undesirable as it sounds, I think the correct thing to do is to
invalidate the existing stale object.  Simply because the cache cannot
know whether it is a temporary error or a deliberate change of state into
an error.  The invalidation is inefficient, but it ensures correctness.

I agree it is real a problem, though.  It seems that the 'stale-if-error'
proposed in RFC 5861 you mentioned was suggested exactly for this purpose.
On the other hand, if you do not have the control over the upstream server
and it responds with a cacheable error (say max-age=3 as in the previous
example), then that will also nuke your stale cache object.

> Use case 2, a cache with non-cacheable private responses:
> 
> A resource has two versions: one is "general" and can/should be 
> cached (e.g., "guest user" version of a page), and another one 
> is private and should not be cached by nginx ("logged in user" 
> version).  The "proxy_cache_bypass" directive is used to determine 
> if a cached version can be returned, or a request to an upstream 
> server is needed.  "Logged in" responses are returned with disabled 
> caching, while "guest user" responses are cacheable.

Fair point, but in this case the cache invalidation logic should take
into account the proxy_cache_bypass condition.  My patch simply did not
address that.

> > 
> > Okay, so what solution do you propose?
> 
> As I already wrote in the very first reply, I'm not even sure a 
> good solution exists.  May be some timeouts like ones proposed by 
> rfc5861 will work (though this will limit various "use stale" cases 
> considerably with low timeouts, and won't help much with high 
> ones).  Or may be we can introduce some counters/heuristics to 
> detect cacheable->uncacheable transitions.  May be just enforcing 
> "inactive" time on such resources regardless of actual requests 
> will work (but unlikely, as an upstream server can be down for a 
> considerable time in some cases).
> 

I would say the right way would be to invalidate the object on 'no-cache',
but provide an nginx option equivalent to 'stale-if-error' logic (or even
better -- a generic directive to override Cache-Control value for a given
HTTP code range).  I understand that breaking the existing configs would
be undesirable.  How about introducing the inverted logic option, e.g.
extending proxy_cache_use_stale with 'invalidate-on-nocache'?

-- 
Mindaugas



More information about the nginx-devel mailing list