[PATCH] Upstream: fix the cache duration calculation
Maxim Dounin
mdounin at mdounin.ru
Thu Nov 14 17:14:35 UTC 2013
Hello!
On Thu, Nov 14, 2013 at 04:33:55PM +0100, Florent Le Coz wrote:
> Hello,
>
> We have a "chain" of nginx servers, like this:
> A (nginx) -> B (nginx) -> C (anything)
>
> B being A’s upstream, and C being B’s upstream
>
> We ran into an issue where the files were being cached for a too
> long duration. For example if we want to cache our files in this
> chain for 10 seconds, it could happen that the file actually exist
> for up to 30 seconds, because when B retrieves the file from C, even
> if it’s almost expired on C, B will cache it for 10 seconds. Same
> thing if A retrieves the file cached by B.
>
> To fix this issue, nginx needs to properly take into account the Age
> header provided by its upstream. This is what the attached patch
> does.
While I agree with problems the patch tries to address (i.e.,
"Cache-Control: max-age" should take precedence over Expires, and
Age should be taken into account while interpreting
"Cache-Control: max-age"), I'm not happy with the patch provided.
Given that there are at least two problems to address, I would
suggest there should be at least two patches: one to address
Expires vs. Cache-Control, and another one to handle Age header.
[...]
> Upstream: fix the cache duration calculation
>
> - Treat Expires and Cache-control headers in a well-defined order, instead
> of treating them in the order they are received from upstream
> - If Expires and Cache-control headers are both present and indicate a
> different cache duration, use the biggest value of the two
As per RFC2616, if Expires and Cache-Control are both present, the
Cache-Control should be used,
http://tools.ietf.org/html/rfc2616#section-14.9.3:
If a response includes both an Expires header and a max-age
directive, the max-age directive overrides the Expires header, even
if the Expires header is more restrictive.
[...]
> @@ -229,6 +233,11 @@
Just a side note: please add
[diff]
showfunc=1
to ~/.hgrc. It makes review much easier. Thanks.
> ngx_http_upstream_copy_header_line,
> offsetof(ngx_http_headers_out_t, expires), 1 },
>
> + { ngx_string("Age"),
> + ngx_http_upstream_process_age, 0,
> + ngx_http_upstream_copy_header_line,
> + offsetof(ngx_http_headers_out_t, age), 1 },
> +
As used in the patch, it looks like there is no need for a special
process function for the Age header.
> { ngx_string("Accept-Ranges"),
> ngx_http_upstream_process_header_line,
> offsetof(ngx_http_upstream_headers_in_t, accept_ranges),
> @@ -2255,6 +2264,7 @@
> /* TODO: preallocate event_pipe bufs, look "Content-Length" */
>
> #if (NGX_HTTP_CACHE)
> + ngx_http_upstream_set_cache_valid(r, u);
>
Processing Cache-Control header in
ngx_http_upstream_send_response() is at least sub-optimal, and may
cause unneeded work if caching is disabled with, e.g.,
"Cache-Control: no-cache".
At most, this should be done in ngx_http_upstream_process_headers(),
but even this will be too late for upcoming cache revalidation
with conditional requests.
[...]
> + if (!(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_CACHE_CONTROL))
> + {
> + u_char *p;
> + u_char *last;
> +
> + ph = u->headers_in.cache_control.elts;
> +
> + for (i = 0; i != u->headers_in.cache_control.nelts; i++)
> + {
> + h = ph[i];
> + p = h->value.data;
> + last = p + h->value.len;
> +
> + if (ngx_strlcasestrn(p, last, (u_char *) "no-cache", 8 - 1) != NULL
> + || ngx_strlcasestrn(p, last, (u_char *) "no-store", 8 - 1) != NULL
> + || ngx_strlcasestrn(p, last, (u_char *) "private", 7 - 1) != NULL)
> + {
> + u->cacheable = 0;
> + return;
> + }
This seems to introduce multiple style problems. It dosn't really
matter though, as it would be much better idea to don't move the
code, see above.
> +
> + p = ngx_strlcasestrn(p, last, (u_char *) "max-age=", 8 - 1);
> +
> + if (p == NULL) {
> + break;
> + }
> +
> + n = 0;
> +
> + for (p += 8; p < last; p++) {
> + if (*p == ',' || *p == ';' || *p == ' ') {
> + break;
> + }
> +
> + if (*p >= '0' && *p <= '9') {
> + n = n * 10 + *p - '0';
> + continue;
> + }
> +
> + u->cacheable = 0;
> + return;
> + }
> +
> + if (n == 0) {
> + u->cacheable = 0;
> + return;
> + }
> + r->cache->valid_sec = ngx_max(r->cache->valid_sec, ngx_time() + n);
The ngx_max() seems to be only meaningful if there are more than one
max-age value found in different Cache-Control headers returned by
a backend.
It is however inconsistent with other code in the function. E.g.,
Cache-Control: max-age=10, max-age=20
will result in 10 seconds max-age, while
Cache-Control: max-age=10
Cache-Control: max-age=20
will result in 20 seconds max-age. Moreover, in contrast to the
previous example,
Cache-Control: max-age=0
Cache-Control: max-age=20
will result in a response being non-cacheable.
> + }
> + }
> + if (!(u->conf->ignore_headers & NGX_HTTP_UPSTREAM_IGN_EXPIRES))
> + {
> + time_t expires;
> +
> + h = u->headers_in.expires;
> + if (h)
> + {
> + expires = ngx_http_parse_time(h->value.data, h->value.len);
> + if (expires == NGX_ERROR || expires < ngx_time()) {
> + u->cacheable = 0;
> + return;
> + }
> + r->cache->valid_sec = ngx_max(expires, r->cache->valid_sec);
This is incorrect and contradicts to RFC2616, see above.
> + }
> + }
> +
> + h = u->headers_in.age;
> + if (h && r->cache->valid_sec)
> + {
> + n = ngx_atoi(h->value.data, h->value.len);
> + if (n > 0 && n != NGX_ERROR)
> + r->cache->valid_sec -= n;
> + }
This will corrupt absolute times parsed from the Expires header.
Additionally, it may result in a valid_sec in the past, which is
not checked.
[...]
--
Maxim Dounin
http://nginx.org/en/donation.html
More information about the nginx-devel
mailing list