Skip to content

Commit a4e3890

Browse files
committed
RequestServer: Implement stale cache revalidation
When a request becomes stale, we will now issue a revalidation request (if the response indicates it may be revalidated). We do this by issuing a normal fetch request, with If-None-Match and/or If-Modified-Since request headers. If the server replies with an HTTP 304 status, we update the stored response headers to match the 304's headers, and serve the response to the client from the cache. If the server replies with any other code, we remove the cache entry. We will open a new cache entry to cache the new response, if possible.
1 parent 3d45a20 commit a4e3890

File tree

9 files changed

+263
-65
lines changed

9 files changed

+263
-65
lines changed

Services/RequestServer/Cache/CacheEntry.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,12 @@ ErrorOr<void> CacheEntryWriter::write_status_and_reason(u32 status_code, Optiona
121121
if (!is_cacheable(status_code, response_headers))
122122
return Error::from_string_literal("Response is not cacheable");
123123

124-
if (auto freshness = calculate_freshness_lifetime(response_headers); freshness.is_negative() || freshness.is_zero())
124+
auto freshness_lifetime = calculate_freshness_lifetime(response_headers);
125+
auto current_age = calculate_age(response_headers, m_request_time, m_response_time);
126+
127+
// We can cache already-expired responses if there are other cache directives that allow us to revalidate the
128+
// response on subsequent requests. For example, `Cache-Control: max-age=0, must-revalidate`.
129+
if (cache_lifetime_status(response_headers, freshness_lifetime, current_age) == CacheLifetimeStatus::Expired)
125130
return Error::from_string_literal("Response has already expired");
126131

127132
TRY(m_file->write_value(m_cache_header));
@@ -240,6 +245,22 @@ CacheEntryReader::CacheEntryReader(DiskCache& disk_cache, CacheIndex& index, u64
240245
{
241246
}
242247

248+
void CacheEntryReader::revalidation_succeeded(HTTP::HeaderMap const& response_headers)
249+
{
250+
dbgln("\033[34;1mCache revalidation succeeded for\033[0m {}", m_url);
251+
252+
update_header_fields(m_response_headers, response_headers);
253+
m_index.update_response_headers(m_cache_key, m_response_headers);
254+
}
255+
256+
void CacheEntryReader::revalidation_failed()
257+
{
258+
dbgln("\033[33;1mCache revalidation failed for\033[0m {}", m_url);
259+
260+
remove();
261+
close_and_destroy_cache_entry();
262+
}
263+
243264
void CacheEntryReader::pipe_to(int pipe_fd, Function<void(u64)> on_complete, Function<void(u64)> on_error)
244265
{
245266
VERIFY(m_pipe_fd == -1);

Services/RequestServer/Cache/CacheEntry.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ class CacheEntryReader : public CacheEntry {
100100
static ErrorOr<NonnullOwnPtr<CacheEntryReader>> create(DiskCache&, CacheIndex&, u64 cache_key, HTTP::HeaderMap, u64 data_size);
101101
virtual ~CacheEntryReader() override = default;
102102

103+
bool must_revalidate() const { return m_must_revalidate; }
104+
void set_must_revalidate() { m_must_revalidate = true; }
105+
106+
void revalidation_succeeded(HTTP::HeaderMap const&);
107+
void revalidation_failed();
108+
103109
void pipe_to(int pipe_fd, Function<void(u64 bytes_piped)> on_complete, Function<void(u64 bytes_piped)> on_error);
104110

105111
u32 status_code() const { return m_cache_header.status_code; }
@@ -128,6 +134,8 @@ class CacheEntryReader : public CacheEntry {
128134
Optional<String> m_reason_phrase;
129135
HTTP::HeaderMap m_response_headers;
130136

137+
bool m_must_revalidate { false };
138+
131139
u64 const m_data_offset { 0 };
132140
u64 const m_data_size { 0 };
133141
};

Services/RequestServer/Cache/CacheIndex.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ ErrorOr<CacheIndex> CacheIndex::create(Database::Database& database)
9999
statements.remove_entry = TRY(database.prepare_statement("DELETE FROM CacheIndex WHERE cache_key = ?;"sv));
100100
statements.remove_all_entries = TRY(database.prepare_statement("DELETE FROM CacheIndex;"sv));
101101
statements.select_entry = TRY(database.prepare_statement("SELECT * FROM CacheIndex WHERE cache_key = ?;"sv));
102+
statements.update_response_headers = TRY(database.prepare_statement("UPDATE CacheIndex SET response_headers = ? WHERE cache_key = ?;"sv));
102103
statements.update_last_access_time = TRY(database.prepare_statement("UPDATE CacheIndex SET last_access_time = ? WHERE cache_key = ?;"sv));
103104

104105
return CacheIndex { database, statements };
@@ -140,6 +141,16 @@ void CacheIndex::remove_all_entries()
140141
m_entries.clear();
141142
}
142143

144+
void CacheIndex::update_response_headers(u64 cache_key, HTTP::HeaderMap response_headers)
145+
{
146+
auto entry = m_entries.get(cache_key);
147+
if (!entry.has_value())
148+
return;
149+
150+
m_database.execute_statement(m_statements.update_response_headers, {}, serialize_headers(response_headers), cache_key);
151+
entry->response_headers = move(response_headers);
152+
}
153+
143154
void CacheIndex::update_last_access_time(u64 cache_key)
144155
{
145156
auto entry = m_entries.get(cache_key);

Services/RequestServer/Cache/CacheIndex.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class CacheIndex {
3939

4040
Optional<Entry&> find_entry(u64 cache_key);
4141

42+
void update_response_headers(u64 cache_key, HTTP::HeaderMap);
4243
void update_last_access_time(u64 cache_key);
4344

4445
private:
@@ -47,6 +48,7 @@ class CacheIndex {
4748
Database::StatementID remove_entry { 0 };
4849
Database::StatementID remove_all_entries { 0 };
4950
Database::StatementID select_entry { 0 };
51+
Database::StatementID update_response_headers { 0 };
5052
Database::StatementID update_last_access_time { 0 };
5153
};
5254

Services/RequestServer/Cache/DiskCache.cpp

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,30 @@ Variant<Optional<CacheEntryReader&>, DiskCache::CacheHasOpenEntry> DiskCache::op
8383
return Optional<CacheEntryReader&> {};
8484
}
8585

86-
auto freshness_lifetime = calculate_freshness_lifetime(cache_entry.value()->response_headers());
87-
auto current_age = calculate_age(cache_entry.value()->response_headers(), index_entry->request_time, index_entry->response_time);
86+
auto const& response_headers = cache_entry.value()->response_headers();
87+
auto freshness_lifetime = calculate_freshness_lifetime(response_headers);
88+
auto current_age = calculate_age(response_headers, index_entry->request_time, index_entry->response_time);
8889

89-
if (!is_response_fresh(freshness_lifetime, current_age)) {
90+
switch (cache_lifetime_status(response_headers, freshness_lifetime, current_age)) {
91+
case CacheLifetimeStatus::Fresh:
92+
dbgln("\033[32;1mOpened disk cache entry for\033[0m {} (lifetime={}s age={}s) ({} bytes)", request.url(), freshness_lifetime.to_seconds(), current_age.to_seconds(), index_entry->data_size);
93+
break;
94+
95+
case CacheLifetimeStatus::Expired:
9096
dbgln("\033[33;1mCache entry expired for\033[0m {} (lifetime={}s age={}s)", request.url(), freshness_lifetime.to_seconds(), current_age.to_seconds());
9197
cache_entry.value()->remove();
9298

9399
return Optional<CacheEntryReader&> {};
94-
}
95100

96-
dbgln("\033[32;1mOpened disk cache entry for\033[0m {} (lifetime={}s age={}s) ({} bytes)", request.url(), freshness_lifetime.to_seconds(), current_age.to_seconds(), index_entry->data_size);
101+
case CacheLifetimeStatus::MustRevalidate:
102+
// We will hold an exclusive lock on the cache entry for revalidation requests.
103+
if (check_if_cache_has_open_entry(request, cache_key, CheckReaderEntries::Yes))
104+
return Optional<CacheEntryReader&> {};
105+
106+
dbgln("\033[36;1mMust revalidate disk cache entry for\033[0m {} (lifetime={}s age={}s)", request.url(), freshness_lifetime.to_seconds(), current_age.to_seconds());
107+
cache_entry.value()->set_must_revalidate();
108+
break;
109+
}
97110

98111
auto* cache_entry_pointer = cache_entry.value().ptr();
99112
m_open_cache_entries.ensure(cache_key).append(cache_entry.release_value());
@@ -113,14 +126,17 @@ bool DiskCache::check_if_cache_has_open_entry(Request& request, u64 cache_key, C
113126
m_requests_waiting_completion.ensure(cache_key).append(request);
114127
return true;
115128
}
116-
}
117129

118-
if (check_reader_entries == CheckReaderEntries::No)
119-
return false;
130+
// We allow concurrent readers unless another reader is open for revalidation. That reader will issue the network
131+
// request, which may then result in the cache entry being updated or deleted.
132+
if (check_reader_entries == CheckReaderEntries::Yes || as<CacheEntryReader>(*open_entry).must_revalidate()) {
133+
dbgln("\033[36;1mDeferring disk cache entry for\033[0m {} (waiting for existing reader)", request.url());
134+
m_requests_waiting_completion.ensure(cache_key).append(request);
135+
return true;
136+
}
137+
}
120138

121-
dbgln("\033[36;1mDeferring disk cache entry for\033[0m {} (waiting for existing reader)", request.url());
122-
m_requests_waiting_completion.ensure(cache_key).append(request);
123-
return true;
139+
return false;
124140
}
125141

126142
void DiskCache::clear_cache()

Services/RequestServer/Cache/Utilities.cpp

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,6 @@ bool is_cacheable(u32 status_code, HTTP::HeaderMap const& headers)
114114
// - a cache extension that allows it to be cached (see Section 5.2.3); or
115115
// - a status code that is defined as heuristically cacheable (see Section 4.2.2).
116116

117-
// FIXME: Implement cache revalidation.
118-
if (cache_control->contains("no-cache"sv, CaseSensitivity::CaseInsensitive))
119-
return false;
120-
if (cache_control->contains("revalidate"sv, CaseSensitivity::CaseInsensitive))
121-
return false;
122-
123117
return true;
124118
}
125119

@@ -216,10 +210,84 @@ AK::Duration calculate_age(HTTP::HeaderMap const& headers, UnixDateTime request_
216210
return AK::Duration::from_seconds(current_age);
217211
}
218212

219-
// https://httpwg.org/specs/rfc9111.html#expiration.model
220-
bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age)
213+
CacheLifetimeStatus cache_lifetime_status(HTTP::HeaderMap const& headers, AK::Duration freshness_lifetime, AK::Duration current_age)
214+
{
215+
auto revalidation_status = [&]() {
216+
// In order to revalidate a cache entry, we must have one of these headers to attach to the revalidation request.
217+
if (headers.contains("Last-Modified"sv) || headers.contains("ETag"sv))
218+
return CacheLifetimeStatus::MustRevalidate;
219+
return CacheLifetimeStatus::Expired;
220+
};
221+
222+
auto cache_control = headers.get("Cache-Control"sv);
223+
224+
// https://httpwg.org/specs/rfc9111.html#cache-response-directive.no-cache
225+
// The no-cache response directive, in its unqualified form (without an argument), indicates that the response MUST
226+
// NOT be used to satisfy any other request without forwarding it for validation and receiving a successful response
227+
//
228+
// FIXME: Handle the qualified form of the no-cache directive, which may allow us to re-use the response.
229+
if (cache_control.has_value() && cache_control->contains("no-cache"sv, CaseSensitivity::CaseInsensitive))
230+
return revalidation_status();
231+
232+
// https://httpwg.org/specs/rfc9111.html#expiration.model
233+
if (freshness_lifetime > current_age)
234+
return CacheLifetimeStatus::Fresh;
235+
236+
if (cache_control.has_value()) {
237+
// https://httpwg.org/specs/rfc9111.html#cache-response-directive.must-revalidate
238+
// The must-revalidate response directive indicates that once the response has become stale, a cache MUST NOT
239+
// reuse that response to satisfy another request until it has been successfully validated by the origin
240+
if (cache_control->contains("must-revalidate"sv, CaseSensitivity::CaseInsensitive))
241+
return revalidation_status();
242+
243+
// FIXME: Implement stale-while-revalidate.
244+
}
245+
246+
return CacheLifetimeStatus::Expired;
247+
}
248+
249+
// https://httpwg.org/specs/rfc9111.html#validation.sent
250+
RevalidationAttributes RevalidationAttributes::create(HTTP::HeaderMap const& headers)
251+
{
252+
RevalidationAttributes attributes;
253+
attributes.etag = headers.get("ETag"sv).map([](auto const& etag) { return etag; });
254+
attributes.last_modified = parse_http_date(headers.get("Last-Modified"sv));
255+
256+
return attributes;
257+
}
258+
259+
// https://httpwg.org/specs/rfc9111.html#update
260+
void update_header_fields(HTTP::HeaderMap& stored_headers, HTTP::HeaderMap const& updated_headers)
221261
{
222-
return freshness_lifetime > current_age;
262+
// Caches are required to update a stored response's header fields from another (typically newer) response in
263+
// several situations; for example, see Sections 3.4, 4.3.4, and 4.3.5.
264+
265+
// When doing so, the cache MUST add each header field in the provided response to the stored response, replacing
266+
// field values that are already present, with the following exceptions:
267+
auto is_header_exempted_from_update = [](StringView name) {
268+
// * Header fields excepted from storage in Section 3.1,
269+
if (is_header_exempted_from_storage(name))
270+
return true;
271+
272+
// * Header fields that the cache's stored response depends upon, as described below,
273+
// * Header fields that are automatically processed and removed by the recipient, as described below, and
274+
275+
// * The Content-Length header field.
276+
if (name.equals_ignoring_ascii_case("Content-Type"sv))
277+
return true;
278+
279+
return false;
280+
};
281+
282+
for (auto const& updated_header : updated_headers.headers()) {
283+
if (!is_header_exempted_from_update(updated_header.name))
284+
stored_headers.remove(updated_header.name);
285+
}
286+
287+
for (auto const& updated_header : updated_headers.headers()) {
288+
if (!is_header_exempted_from_update(updated_header.name))
289+
stored_headers.set(updated_header.name, updated_header.value);
290+
}
223291
}
224292

225293
}

Services/RequestServer/Cache/Utilities.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ bool is_header_exempted_from_storage(StringView name);
2323

2424
AK::Duration calculate_freshness_lifetime(HTTP::HeaderMap const&);
2525
AK::Duration calculate_age(HTTP::HeaderMap const&, UnixDateTime request_time, UnixDateTime response_time);
26-
bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age);
26+
27+
enum class CacheLifetimeStatus {
28+
Fresh,
29+
Expired,
30+
MustRevalidate,
31+
};
32+
CacheLifetimeStatus cache_lifetime_status(HTTP::HeaderMap const&, AK::Duration freshness_lifetime, AK::Duration current_age);
33+
34+
struct RevalidationAttributes {
35+
static RevalidationAttributes create(HTTP::HeaderMap const&);
36+
37+
Optional<ByteString> etag;
38+
Optional<UnixDateTime> last_modified;
39+
};
40+
41+
void update_header_fields(HTTP::HeaderMap&, HTTP::HeaderMap const&);
2742

2843
}

0 commit comments

Comments
 (0)