Skip to content

Commit 4de3f77

Browse files
trflynn89gmta
authored andcommitted
RequestServer: Add a hook to advance a request's clock time for testing
For example, we will want to be able to test that a cached object was expired after N seconds. Rather than waiting that time during testing, this adds a testing-only request header to internally advance the clock for a single HTTP request.
1 parent b2c112c commit 4de3f77

File tree

7 files changed

+50
-22
lines changed

7 files changed

+50
-22
lines changed

Services/RequestServer/Cache/CacheEntry.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ void CacheEntry::close_and_destroy_cache_entry()
7676
m_disk_cache.cache_entry_closed({}, *this);
7777
}
7878

79-
ErrorOr<NonnullOwnPtr<CacheEntryWriter>> CacheEntryWriter::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, UnixDateTime request_time)
79+
ErrorOr<NonnullOwnPtr<CacheEntryWriter>> CacheEntryWriter::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, UnixDateTime request_time, AK::Duration current_time_offset_for_testing)
8080
{
8181
auto path = path_for_cache_key(disk_cache.cache_directory(), cache_key);
8282

@@ -87,14 +87,15 @@ ErrorOr<NonnullOwnPtr<CacheEntryWriter>> CacheEntryWriter::create(DiskCache& dis
8787
cache_header.url_size = url.byte_count();
8888
cache_header.url_hash = url.hash();
8989

90-
return adopt_own(*new CacheEntryWriter { disk_cache, index, cache_key, move(url), move(path), move(file), cache_header, request_time });
90+
return adopt_own(*new CacheEntryWriter { disk_cache, index, cache_key, move(url), move(path), move(file), cache_header, request_time, current_time_offset_for_testing });
9191
}
9292

93-
CacheEntryWriter::CacheEntryWriter(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr<Core::OutputBufferedFile> file, CacheHeader cache_header, UnixDateTime request_time)
93+
CacheEntryWriter::CacheEntryWriter(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr<Core::OutputBufferedFile> file, CacheHeader cache_header, UnixDateTime request_time, AK::Duration current_time_offset_for_testing)
9494
: CacheEntry(disk_cache, index, cache_key, move(url), move(path), cache_header)
9595
, m_file(move(file))
9696
, m_request_time(request_time)
97-
, m_response_time(UnixDateTime::now())
97+
, m_response_time(UnixDateTime::now() + current_time_offset_for_testing)
98+
, m_current_time_offset_for_testing(current_time_offset_for_testing)
9899
{
99100
}
100101

@@ -116,8 +117,8 @@ ErrorOr<void> CacheEntryWriter::write_status_and_reason(u32 status_code, Optiona
116117
if (!is_cacheable(status_code, response_headers))
117118
return Error::from_string_literal("Response is not cacheable");
118119

119-
auto freshness_lifetime = calculate_freshness_lifetime(status_code, response_headers);
120-
auto current_age = calculate_age(response_headers, m_request_time, m_response_time);
120+
auto freshness_lifetime = calculate_freshness_lifetime(status_code, response_headers, m_current_time_offset_for_testing);
121+
auto current_age = calculate_age(response_headers, m_request_time, m_response_time, m_current_time_offset_for_testing);
121122

122123
// We can cache already-expired responses if there are other cache directives that allow us to revalidate the
123124
// response on subsequent requests. For example, `Cache-Control: max-age=0, must-revalidate`.

Services/RequestServer/Cache/CacheEntry.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,22 @@ class CacheEntry {
7979

8080
class CacheEntryWriter : public CacheEntry {
8181
public:
82-
static ErrorOr<NonnullOwnPtr<CacheEntryWriter>> create(DiskCache&, CacheIndex&, u64 cache_key, String url, UnixDateTime request_time);
82+
static ErrorOr<NonnullOwnPtr<CacheEntryWriter>> create(DiskCache&, CacheIndex&, u64 cache_key, String url, UnixDateTime request_time, AK::Duration current_time_offset_for_testing);
8383
virtual ~CacheEntryWriter() override = default;
8484

8585
ErrorOr<void> write_status_and_reason(u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const&);
8686
ErrorOr<void> write_data(ReadonlyBytes);
8787
ErrorOr<void> flush(HTTP::HeaderMap);
8888

8989
private:
90-
CacheEntryWriter(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr<Core::OutputBufferedFile>, CacheHeader, UnixDateTime request_time);
90+
CacheEntryWriter(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr<Core::OutputBufferedFile>, CacheHeader, UnixDateTime request_time, AK::Duration current_time_offset_for_testing);
9191

9292
NonnullOwnPtr<Core::OutputBufferedFile> m_file;
9393

9494
UnixDateTime m_request_time;
9595
UnixDateTime m_response_time;
96+
97+
AK::Duration m_current_time_offset_for_testing;
9698
};
9799

98100
class CacheEntryReader : public CacheEntry {

Services/RequestServer/Cache/DiskCache.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Variant<Optional<CacheEntryWriter&>, DiskCache::CacheHasOpenEntry> DiskCache::cr
5353
if (check_if_cache_has_open_entry(request, cache_key, CheckReaderEntries::Yes))
5454
return CacheHasOpenEntry {};
5555

56-
auto cache_entry = CacheEntryWriter::create(*this, m_index, cache_key, move(serialized_url), request.request_start_time());
56+
auto cache_entry = CacheEntryWriter::create(*this, m_index, cache_key, move(serialized_url), request.request_start_time(), request.current_time_offset_for_testing());
5757
if (cache_entry.is_error()) {
5858
dbgln("\033[31;1mUnable to create cache entry for\033[0m {}: {}", request.url(), cache_entry.error());
5959
return Optional<CacheEntryWriter&> {};
@@ -93,8 +93,8 @@ Variant<Optional<CacheEntryReader&>, DiskCache::CacheHasOpenEntry> DiskCache::op
9393
}
9494

9595
auto const& response_headers = cache_entry.value()->response_headers();
96-
auto freshness_lifetime = calculate_freshness_lifetime(cache_entry.value()->status_code(), response_headers);
97-
auto current_age = calculate_age(response_headers, index_entry->request_time, index_entry->response_time);
96+
auto freshness_lifetime = calculate_freshness_lifetime(cache_entry.value()->status_code(), response_headers, request.current_time_offset_for_testing());
97+
auto current_age = calculate_age(response_headers, index_entry->request_time, index_entry->response_time, request.current_time_offset_for_testing());
9898

9999
switch (cache_lifetime_status(response_headers, freshness_lifetime, current_age)) {
100100
case CacheLifetimeStatus::Fresh:

Services/RequestServer/Cache/Utilities.cpp

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <LibCrypto/Hash/SHA1.h>
88
#include <LibURL/URL.h>
9+
#include <RequestServer/Cache/DiskCache.h>
910
#include <RequestServer/Cache/Utilities.h>
1011

1112
namespace RequestServer {
@@ -207,11 +208,12 @@ bool is_header_exempted_from_storage(StringView name)
207208

208209
// AD-HOC: Exclude headers used only for testing.
209210
TEST_CACHE_ENABLED_HEADER,
210-
TEST_CACHE_STATUS_HEADER);
211+
TEST_CACHE_STATUS_HEADER,
212+
TEST_CACHE_REQUEST_TIME_OFFSET);
211213
}
212214

213215
// https://httpwg.org/specs/rfc9111.html#heuristic.freshness
214-
static AK::Duration calculate_heuristic_freshness_lifetime(HTTP::HeaderMap const& headers)
216+
static AK::Duration calculate_heuristic_freshness_lifetime(HTTP::HeaderMap const& headers, AK::Duration current_time_offset_for_testing)
215217
{
216218
// Since origin servers do not always provide explicit expiration times, a cache MAY assign a heuristic expiration
217219
// time when an explicit time is not specified, employing algorithms that use other field values (such as the
@@ -230,7 +232,7 @@ static AK::Duration calculate_heuristic_freshness_lifetime(HTTP::HeaderMap const
230232
if (!last_modified.has_value())
231233
return {};
232234

233-
auto now = UnixDateTime::now();
235+
auto now = UnixDateTime::now() + current_time_offset_for_testing;
234236
auto since_last_modified = now - *last_modified;
235237
auto seconds = since_last_modified.to_seconds();
236238

@@ -243,7 +245,7 @@ static AK::Duration calculate_heuristic_freshness_lifetime(HTTP::HeaderMap const
243245
}
244246

245247
// https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
246-
AK::Duration calculate_freshness_lifetime(u32 status_code, HTTP::HeaderMap const& headers)
248+
AK::Duration calculate_freshness_lifetime(u32 status_code, HTTP::HeaderMap const& headers, AK::Duration current_time_offset_for_testing)
247249
{
248250
// A cache can calculate the freshness lifetime (denoted as freshness_lifetime) of a response by evaluating the
249251
// following rules and using the first match:
@@ -265,8 +267,8 @@ AK::Duration calculate_freshness_lifetime(u32 status_code, HTTP::HeaderMap const
265267
// * If the Expires response header field (Section 5.3) is present, use its value minus the value of the Date response
266268
// header field (using the time the message was received if it is not present, as per Section 6.6.1 of [HTTP]), or
267269
if (auto expires = parse_http_date(headers.get("Expires"sv)); expires.has_value()) {
268-
auto date = parse_http_date(headers.get("Date"sv)).value_or_lazy_evaluated([]() {
269-
return UnixDateTime::now();
270+
auto date = parse_http_date(headers.get("Date"sv)).value_or_lazy_evaluated([&]() {
271+
return UnixDateTime::now() + current_time_offset_for_testing;
270272
});
271273

272274
return *expires - date;
@@ -287,14 +289,14 @@ AK::Duration calculate_freshness_lifetime(u32 status_code, HTTP::HeaderMap const
287289
}
288290

289291
if (heuristics_allowed)
290-
return calculate_heuristic_freshness_lifetime(headers);
292+
return calculate_heuristic_freshness_lifetime(headers, current_time_offset_for_testing);
291293

292294
// No explicit expiration time, and heuristics not allowed or not applicable.
293295
return {};
294296
}
295297

296298
// https://httpwg.org/specs/rfc9111.html#age.calculations
297-
AK::Duration calculate_age(HTTP::HeaderMap const& headers, UnixDateTime request_time, UnixDateTime response_time)
299+
AK::Duration calculate_age(HTTP::HeaderMap const& headers, UnixDateTime request_time, UnixDateTime response_time, AK::Duration current_time_offset_for_testing)
298300
{
299301
// The term "age_value" denotes the value of the Age header field (Section 5.1), in a form appropriate for arithmetic
300302
// operation; or 0, if not available.
@@ -306,7 +308,7 @@ AK::Duration calculate_age(HTTP::HeaderMap const& headers, UnixDateTime request_
306308
}
307309

308310
// The term "now" means the current value of this implementation's clock (Section 5.6.7 of [HTTP]).
309-
auto now = UnixDateTime::now();
311+
auto now = UnixDateTime::now() + current_time_offset_for_testing;
310312

311313
// The term "date_value" denotes the value of the Date header field, in a form appropriate for arithmetic operations.
312314
// See Section 6.6.1 of [HTTP] for the definition of the Date header field and for requirements regarding responses
@@ -406,4 +408,16 @@ void update_header_fields(HTTP::HeaderMap& stored_headers, HTTP::HeaderMap const
406408
}
407409
}
408410

411+
AK::Duration compute_current_time_offset_for_testing(Optional<DiskCache&> disk_cache, HTTP::HeaderMap const& request_headers)
412+
{
413+
if (disk_cache.has_value() && disk_cache->mode() == DiskCache::Mode::Testing) {
414+
if (auto header = request_headers.get(TEST_CACHE_REQUEST_TIME_OFFSET); header.has_value()) {
415+
if (auto offset = header->to_number<i64>(); offset.has_value())
416+
return AK::Duration::from_seconds(*offset);
417+
}
418+
}
419+
420+
return {};
421+
}
422+
409423
}

Services/RequestServer/Cache/Utilities.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
#include <AK/Types.h>
1313
#include <LibHTTP/HeaderMap.h>
1414
#include <LibURL/Forward.h>
15+
#include <RequestServer/Forward.h>
1516

1617
namespace RequestServer {
1718

1819
constexpr inline auto TEST_CACHE_ENABLED_HEADER = "X-Ladybird-Enable-Disk-Cache"sv;
1920
constexpr inline auto TEST_CACHE_STATUS_HEADER = "X-Ladybird-Disk-Cache-Status"sv;
21+
constexpr inline auto TEST_CACHE_REQUEST_TIME_OFFSET = "X-Ladybird-Request-Time-Offset"sv;
2022

2123
String serialize_url_for_cache_storage(URL::URL const&);
2224
u64 create_cache_key(StringView url, StringView method);
@@ -26,8 +28,8 @@ bool is_cacheable(StringView method);
2628
bool is_cacheable(u32 status_code, HTTP::HeaderMap const&);
2729
bool is_header_exempted_from_storage(StringView name);
2830

29-
AK::Duration calculate_freshness_lifetime(u32 status_code, HTTP::HeaderMap const&);
30-
AK::Duration calculate_age(HTTP::HeaderMap const&, UnixDateTime request_time, UnixDateTime response_time);
31+
AK::Duration calculate_freshness_lifetime(u32 status_code, HTTP::HeaderMap const&, AK::Duration current_time_offset_for_testing);
32+
AK::Duration calculate_age(HTTP::HeaderMap const&, UnixDateTime request_time, UnixDateTime response_time, AK::Duration current_time_offset_for_testing);
3133

3234
enum class CacheLifetimeStatus {
3335
Fresh,
@@ -45,4 +47,6 @@ struct RevalidationAttributes {
4547

4648
void update_header_fields(HTTP::HeaderMap&, HTTP::HeaderMap const&);
4749

50+
AK::Duration compute_current_time_offset_for_testing(Optional<DiskCache&>, HTTP::HeaderMap const& request_headers);
51+
4852
}

Services/RequestServer/Request.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ Request::Request(
8585
, m_request_body(move(request_body))
8686
, m_alt_svc_cache_path(move(alt_svc_cache_path))
8787
, m_proxy_data(proxy_data)
88+
, m_current_time_offset_for_testing(compute_current_time_offset_for_testing(m_disk_cache, m_request_headers))
8889
{
90+
m_request_start_time += m_current_time_offset_for_testing;
8991
}
9092

9193
Request::Request(
@@ -100,7 +102,9 @@ Request::Request(
100102
, m_curl_multi_handle(curl_multi)
101103
, m_resolver(resolver)
102104
, m_url(move(url))
105+
, m_current_time_offset_for_testing(compute_current_time_offset_for_testing(m_disk_cache, m_request_headers))
103106
{
107+
m_request_start_time += m_current_time_offset_for_testing;
104108
}
105109

106110
Request::~Request()

Services/RequestServer/Request.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class Request : public Weakable<Request> {
5555
ByteString const& method() const { return m_method; }
5656
HTTP::HeaderMap const& request_headers() const { return m_request_headers; }
5757
UnixDateTime request_start_time() const { return m_request_start_time; }
58+
AK::Duration current_time_offset_for_testing() const { return m_current_time_offset_for_testing; }
5859

5960
void notify_request_unblocked(Badge<DiskCache>);
6061
void notify_fetch_complete(Badge<ConnectionFromClient>, int result_code);
@@ -170,6 +171,8 @@ class Request : public Weakable<Request> {
170171
CacheStatus m_cache_status { CacheStatus::Unknown };
171172

172173
Optional<Requests::NetworkError> m_network_error;
174+
175+
AK::Duration m_current_time_offset_for_testing;
173176
};
174177

175178
}

0 commit comments

Comments
 (0)