[PATCH 1 of 2] Cache: added calculation of the Age header
Hiroaki Nakamura
hnakamur at gmail.com
Mon Jul 1 13:46:54 UTC 2024
# HG changeset patch
# User Hiroaki Nakamura <hnakamur at gmail.com>
# Date 1719839232 -32400
# Mon Jul 01 22:07:12 2024 +0900
# Node ID 685f47b210263988886db6fe15a95e177616de1f
# Parent cef4919b830f6e149d7811fa22130da4135f4f90
Cache: added calculation of the Age header.
* Implement the modified calculation of the Age header as specified in
RFC 9111 "HTTP Caching", https://www.rfc-editor.org/rfc/rfc9111.html
Calculation initial age as specified in RFC 9111:
apparent_age = max(0, response_time - date_value);
response_delay = response_time - request_time;
corrected_age_value = age_value + response_delay;
corrected_initial_age = max(apparent_age, corrected_age_value);
However, we ignore apparent_age and response_delay and just use:
corrected_initial_age = age_value;
We adjust valid_sec of ngx_http_cache_t by decreasing by the
age value received from the upstream only when Cache-Control
max-age or s-maxage is used.
(For proxy_cache_valid directive, Expires, and X-Accel-Expires
we do not adjust valid_sec).
When sending a cached response, we calculate the Age
as below:
resident_time = now - response_time;
current_age = initial_age + resident_time;
where response_time is cache_creation_time.
* Also ignore Expires when Cache-Control max-age
or s-maxage is set as specified in
https://www.rfc-editor.org/rfc/rfc9111#name-expires:
"If a response includes a Cache-Control header field with the
max-age directive (Section 5.2.2.1), a recipient MUST ignore
the Expires header field. Likewise, if a response includes the
s-maxage directive (Section 5.2.2.10), a shared cache recipient
MUST ignore the Expires header field."
Note X-Accel-Expires has higher precedence over Cache-Control
so the modification of the Age header is not applied.
diff -r cef4919b830f -r 685f47b21026 src/http/ngx_http_header_filter_module.c
--- a/src/http/ngx_http_header_filter_module.c Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/ngx_http_header_filter_module.c Mon Jul 01 22:07:12 2024 +0900
@@ -322,6 +322,10 @@ ngx_http_header_filter(ngx_http_request_
len += sizeof("Last-Modified: Mon, 28 Sep 1970 06:00:00 GMT" CRLF) - 1;
}
+ if (r->headers_out.age_n != -1) {
+ len += sizeof("Age: ") - 1 + NGX_TIME_T_LEN + 2;
+ }
+
c = r->connection;
if (r->headers_out.location
@@ -518,6 +522,10 @@ ngx_http_header_filter(ngx_http_request_
*b->last++ = CR; *b->last++ = LF;
}
+ if (r->headers_out.age_n != -1) {
+ b->last = ngx_sprintf(b->last, "Age: %T" CRLF, r->headers_out.age_n);
+ }
+
if (host.data) {
p = b->last + sizeof("Location: ") - 1;
diff -r cef4919b830f -r 685f47b21026 src/http/ngx_http_request.c
--- a/src/http/ngx_http_request.c Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/ngx_http_request.c Mon Jul 01 22:07:12 2024 +0900
@@ -646,6 +646,7 @@ ngx_http_alloc_request(ngx_connection_t
r->headers_in.keep_alive_n = -1;
r->headers_out.content_length_n = -1;
r->headers_out.last_modified_time = -1;
+ r->headers_out.age_n = -1;
r->uri_changes = NGX_HTTP_MAX_URI_CHANGES + 1;
r->subrequests = NGX_HTTP_MAX_SUBREQUESTS + 1;
diff -r cef4919b830f -r 685f47b21026 src/http/ngx_http_request.h
--- a/src/http/ngx_http_request.h Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/ngx_http_request.h Mon Jul 01 22:07:12 2024 +0900
@@ -291,6 +291,7 @@ typedef struct {
off_t content_offset;
time_t date_time;
time_t last_modified_time;
+ time_t age_n;
} ngx_http_headers_out_t;
diff -r cef4919b830f -r 685f47b21026 src/http/ngx_http_special_response.c
--- a/src/http/ngx_http_special_response.c Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/ngx_http_special_response.c Mon Jul 01 22:07:12 2024 +0900
@@ -581,6 +581,7 @@ ngx_http_clean_header(ngx_http_request_t
r->headers_out.content_length_n = -1;
r->headers_out.last_modified_time = -1;
+ r->headers_out.age_n = -1;
}
diff -r cef4919b830f -r 685f47b21026 src/http/ngx_http_upstream.c
--- a/src/http/ngx_http_upstream.c Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/ngx_http_upstream.c Mon Jul 01 22:07:12 2024 +0900
@@ -136,6 +136,8 @@ static ngx_int_t
ngx_table_elt_t *h, ngx_uint_t offset);
static ngx_int_t ngx_http_upstream_process_vary(ngx_http_request_t *r,
ngx_table_elt_t *h, ngx_uint_t offset);
+static ngx_int_t ngx_http_upstream_process_age(ngx_http_request_t *r,
+ ngx_table_elt_t *h, ngx_uint_t offset);
static ngx_int_t ngx_http_upstream_copy_header_line(ngx_http_request_t *r,
ngx_table_elt_t *h, ngx_uint_t offset);
static ngx_int_t
@@ -323,6 +325,10 @@ static ngx_http_upstream_header_t ngx_h
ngx_http_upstream_copy_header_line,
offsetof(ngx_http_headers_out_t, content_encoding), 0 },
+ { ngx_string("Age"),
+ ngx_http_upstream_process_age, 0,
+ ngx_http_upstream_ignore_header_line, 0, 0 },
+
{ ngx_null_string, NULL, 0, NULL, 0, 0 }
};
@@ -507,6 +513,7 @@ ngx_http_upstream_create(ngx_http_reques
u->headers_in.content_length_n = -1;
u->headers_in.last_modified_time = -1;
+ u->headers_in.age_n = -1;
return NGX_OK;
}
@@ -961,6 +968,7 @@ ngx_http_upstream_cache(ngx_http_request
c->updating_sec = 0;
c->error_sec = 0;
+ u->headers_in.relative_freshness = 0;
u->buffer.start = NULL;
u->cache_status = NGX_HTTP_CACHE_EXPIRED;
@@ -1057,6 +1065,7 @@ ngx_http_upstream_cache_get(ngx_http_req
static ngx_int_t
ngx_http_upstream_cache_send(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
+ time_t age_when_revalidated;
ngx_int_t rc;
ngx_http_cache_t *c;
@@ -1068,6 +1077,8 @@ ngx_http_upstream_cache_send(ngx_http_re
return ngx_http_cache_send(r);
}
+ age_when_revalidated = u->headers_in.age_n;
+
/* TODO: cache stack */
u->buffer = *c->buf;
@@ -1076,6 +1087,7 @@ ngx_http_upstream_cache_send(ngx_http_re
ngx_memzero(&u->headers_in, sizeof(ngx_http_upstream_headers_in_t));
u->headers_in.content_length_n = -1;
u->headers_in.last_modified_time = -1;
+ u->headers_in.age_n = -1;
if (ngx_list_init(&u->headers_in.headers, r->pool, 8,
sizeof(ngx_table_elt_t))
@@ -1094,6 +1106,10 @@ ngx_http_upstream_cache_send(ngx_http_re
rc = u->process_header(r);
if (rc == NGX_OK) {
+ if (u->cache_status == NGX_HTTP_CACHE_REVALIDATED) {
+ /* use age from upstream when revalidated */
+ u->headers_in.age_n = age_when_revalidated;
+ }
rc = ngx_http_upstream_process_headers(r, u);
@@ -2016,6 +2032,7 @@ ngx_http_upstream_reinit(ngx_http_reques
ngx_memzero(&u->headers_in, sizeof(ngx_http_upstream_headers_in_t));
u->headers_in.content_length_n = -1;
u->headers_in.last_modified_time = -1;
+ u->headers_in.age_n = -1;
if (ngx_list_init(&u->headers_in.headers, r->pool, 8,
sizeof(ngx_table_elt_t))
@@ -2977,6 +2994,42 @@ ngx_http_upstream_process_headers(ngx_ht
r->headers_out.status_line = u->headers_in.status_line;
r->headers_out.content_length_n = u->headers_in.content_length_n;
+ r->headers_out.age_n = u->headers_in.age_n;
+
+#if (NGX_HTTP_CACHE)
+ if (r->cache) {
+ ngx_http_cache_t *c;
+
+ c = r->cache;
+
+ if (u->cache_status == NGX_HTTP_CACHE_HIT
+ || u->cache_status == NGX_HTTP_CACHE_STALE)
+ {
+ time_t resident_time, age_value;
+
+ /*
+ * Update age response header.
+ * https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-age
+ *
+ * resident_time = now - response_time;
+ * current_age = corrected_initial_age + resident_time;
+ */
+ age_value = u->headers_in.age_n != -1 ? u->headers_in.age_n : 0;
+ resident_time = ngx_time() - c->date;
+ r->headers_out.age_n = age_value + resident_time;
+
+ } else if (u->cache_status == NGX_HTTP_CACHE_MISS
+ || u->cache_status == NGX_HTTP_CACHE_REVALIDATED)
+ {
+ if (u->headers_in.relative_freshness) {
+ time_t age_value;
+ /* we treat non existent age header as age: 0 */
+ age_value = u->headers_in.age_n != -1 ?
u->headers_in.age_n : 0;
+ c->valid_sec -= age_value;
+ }
+ }
+ }
+#endif
r->disable_not_modified = !u->cacheable;
@@ -4945,6 +4998,7 @@ ngx_http_upstream_process_cache_control(
}
r->cache->valid_sec = ngx_time() + n;
+ u->headers_in.relative_freshness = 1;
u->headers_in.expired = 0;
}
@@ -5047,7 +5101,18 @@ ngx_http_upstream_process_expires(ngx_ht
return NGX_OK;
}
- r->cache->valid_sec = expires;
+ /*
+ * https://www.rfc-editor.org/rfc/rfc9111#name-expires
+ *
+ * If a response includes a Cache-Control header field with the max-age
+ * directive (Section 5.2.2.1), a recipient MUST ignore the Expires
+ * header field. Likewise, if a response includes the s-maxage directive
+ * (Section 5.2.2.10), a shared cache recipient MUST ignore the Expires
+ * header field.
+ */
+ if (!u->headers_in.relative_freshness) {
+ r->cache->valid_sec = expires;
+ }
}
#endif
@@ -5119,7 +5184,8 @@ ngx_http_upstream_process_accel_expires(
n = ngx_atoi(p, len);
if (n != NGX_ERROR) {
- r->cache->valid_sec = n;
+ /* X-Accel-Expires has higher precedence over Cache-Control. */
+ u->headers_in.relative_freshness = 0;
u->headers_in.no_cache = 0;
u->headers_in.expired = 0;
}
@@ -5373,6 +5439,38 @@ ngx_http_upstream_process_vary(ngx_http_
static ngx_int_t
+ngx_http_upstream_process_age(ngx_http_request_t *r,
+ ngx_table_elt_t *h, ngx_uint_t offset)
+{
+ ngx_http_upstream_t *u;
+
+ u = r->upstream;
+
+ if (u->headers_in.age) {
+ ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+ "ignore duplicate age header from upstream: \"%V: %V\", "
+ "previous value: \"%V: %V\"",
+ &h->key, &h->value,
+ &u->headers_in.age->key,
+ &u->headers_in.age->value);
+ return NGX_OK;
+ }
+
+ h->next = NULL;
+ u->headers_in.age = h;
+ u->headers_in.age_n = ngx_atotm(h->value.data, h->value.len);
+
+ if (u->headers_in.age_n == NGX_ERROR) {
+ ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
+ "ignore invalid \"Age\" header from upstream: "
+ "\"%V: %V\"", &h->key, &h->value);
+ }
+
+ return NGX_OK;
+}
+
+
+static ngx_int_t
ngx_http_upstream_copy_header_line(ngx_http_request_t *r, ngx_table_elt_t *h,
ngx_uint_t offset)
{
diff -r cef4919b830f -r 685f47b21026 src/http/ngx_http_upstream.h
--- a/src/http/ngx_http_upstream.h Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/ngx_http_upstream.h Mon Jul 01 22:07:12 2024 +0900
@@ -287,14 +287,17 @@ typedef struct {
ngx_table_elt_t *cache_control;
ngx_table_elt_t *set_cookie;
+ ngx_table_elt_t *age;
off_t content_length_n;
time_t last_modified_time;
+ time_t age_n;
unsigned connection_close:1;
unsigned chunked:1;
unsigned no_cache:1;
unsigned expired:1;
+ unsigned relative_freshness:1;
} ngx_http_upstream_headers_in_t;
diff -r cef4919b830f -r 685f47b21026 src/http/v2/ngx_http_v2.h
--- a/src/http/v2/ngx_http_v2.h Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/v2/ngx_http_v2.h Mon Jul 01 22:07:12 2024 +0900
@@ -398,6 +398,7 @@ ngx_int_t ngx_http_v2_table_size(ngx_htt
#define NGX_HTTP_V2_STATUS_404_INDEX 13
#define NGX_HTTP_V2_STATUS_500_INDEX 14
+#define NGX_HTTP_V2_AGE_INDEX 21
#define NGX_HTTP_V2_CONTENT_LENGTH_INDEX 28
#define NGX_HTTP_V2_CONTENT_TYPE_INDEX 31
#define NGX_HTTP_V2_DATE_INDEX 33
diff -r cef4919b830f -r 685f47b21026 src/http/v2/ngx_http_v2_filter_module.c
--- a/src/http/v2/ngx_http_v2_filter_module.c Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/v2/ngx_http_v2_filter_module.c Mon Jul 01 22:07:12 2024 +0900
@@ -258,6 +258,10 @@ ngx_http_v2_header_filter(ngx_http_reque
len += 1 + ngx_http_v2_literal_size("Wed, 31 Dec 1986 18:00:00 GMT");
}
+ if (r->headers_out.age_n != -1) {
+ len += 1 + ngx_http_v2_integer_octets(NGX_TIME_T_LEN) + NGX_TIME_T_LEN;
+ }
+
if (r->headers_out.location && r->headers_out.location->value.len) {
if (r->headers_out.location->value.data[0] == '/'
@@ -552,6 +556,18 @@ ngx_http_v2_header_filter(ngx_http_reque
pos = ngx_http_v2_write_value(pos, pos, len, tmp);
}
+ if (r->headers_out.age_n != -1) {
+ ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0,
+ "http2 output header: \"age: %T\"",
+ r->headers_out.age_n);
+
+ *pos++ = ngx_http_v2_inc_indexed(NGX_HTTP_V2_AGE_INDEX);
+
+ p = pos;
+ pos = ngx_sprintf(pos + 1, "%T", r->headers_out.age_n);
+ *p = NGX_HTTP_V2_ENCODE_RAW | (u_char) (pos - p - 1);
+ }
+
if (r->headers_out.location && r->headers_out.location->value.len) {
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0,
"http2 output header: \"location: %V\"",
diff -r cef4919b830f -r 685f47b21026 src/http/v3/ngx_http_v3_filter_module.c
--- a/src/http/v3/ngx_http_v3_filter_module.c Mon Jul 01 22:07:03 2024 +0900
+++ b/src/http/v3/ngx_http_v3_filter_module.c Mon Jul 01 22:07:12 2024 +0900
@@ -13,6 +13,7 @@
/* static table indices */
#define NGX_HTTP_V3_HEADER_AUTHORITY 0
#define NGX_HTTP_V3_HEADER_PATH_ROOT 1
+#define NGX_HTTP_V3_HEADER_AGE_ZERO 2
#define NGX_HTTP_V3_HEADER_CONTENT_LENGTH_ZERO 4
#define NGX_HTTP_V3_HEADER_DATE 6
#define NGX_HTTP_V3_HEADER_LAST_MODIFIED 10
@@ -213,6 +214,15 @@ ngx_http_v3_header_filter(ngx_http_reque
sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1);
}
+ if (r->headers_out.age_n > 0) {
+ len += ngx_http_v3_encode_field_lri(NULL, 0,
+ NGX_HTTP_V3_HEADER_AGE_ZERO,
+ NULL, NGX_TIME_T_LEN);
+ } else if (r->headers_out.age_n == 0) {
+ len += ngx_http_v3_encode_field_ri(NULL, 0,
+ NGX_HTTP_V3_HEADER_AGE_ZERO);
+ }
+
if (r->headers_out.location && r->headers_out.location->value.len) {
if (r->headers_out.location->value.data[0] == '/'
@@ -452,6 +462,27 @@ ngx_http_v3_header_filter(ngx_http_reque
p, n);
}
+ if (r->headers_out.age_n != -1) {
+ ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
+ "http3 output header: \"age: %T\"",
+ r->headers_out.age_n);
+
+ if (r->headers_out.age_n > 0) {
+ p = ngx_sprintf(b->last, "%T", r->headers_out.age_n);
+ n = p - b->last;
+
+ b->last = (u_char *) ngx_http_v3_encode_field_lri(b->last, 0,
+ NGX_HTTP_V3_HEADER_AGE_ZERO,
+ NULL, n);
+
+ b->last = ngx_sprintf(b->last, "%T", r->headers_out.age_n);
+
+ } else {
+ b->last = (u_char *) ngx_http_v3_encode_field_ri(b->last, 0,
+ NGX_HTTP_V3_HEADER_AGE_ZERO);
+ }
+ }
+
if (r->headers_out.location && r->headers_out.location->value.len) {
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
"http3 output header: \"location: %V\"",
More information about the nginx-devel
mailing list