add response header when stale data is served
This commit is contained in:
parent
c65a034bf0
commit
0fc03e9a20
3 changed files with 83 additions and 2 deletions
|
|
@ -159,6 +159,11 @@ fn handleWeatherInternal(
|
||||||
};
|
};
|
||||||
defer weather.deinit();
|
defer weather.deinit();
|
||||||
|
|
||||||
|
// The upstream is down and this is old data. Say so, with how long past its
|
||||||
|
// expiry it is, so scripts and status bars can decide whether to trust it.
|
||||||
|
if (weather.stale_for_s) |stale_for_s|
|
||||||
|
res.headers.add("X-Weather-Stale", try std.fmt.allocPrint(res.arena, "{d}", .{stale_for_s}));
|
||||||
|
|
||||||
// Set display name for rendering
|
// Set display name for rendering
|
||||||
weather.display_name = try req_alloc.dupe(u8, location.name);
|
weather.display_name = try req_alloc.dupe(u8, location.name);
|
||||||
|
|
||||||
|
|
@ -771,3 +776,68 @@ test "handler: upstream weather failure with nothing cached is a retryable 503"
|
||||||
try ht.expectStatus(503);
|
try ht.expectStatus(503);
|
||||||
try ht.expectHeader("Retry-After", "60");
|
try ht.expectHeader("Retry-After", "60");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "handler: stale data is served with an X-Weather-Stale header" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const MockHarness = @import("Server.zig").MockHarness;
|
||||||
|
const Pins = @import("../location/Pins.zig");
|
||||||
|
const Location = @import("../location/resolver.zig").Location;
|
||||||
|
const Cache = @import("../cache/Cache.zig");
|
||||||
|
|
||||||
|
var harness = try MockHarness.init(allocator);
|
||||||
|
defer harness.deinit();
|
||||||
|
harness.mock.fail_with = error.WeatherApiFailed;
|
||||||
|
|
||||||
|
// Pinned so the cache key does not depend on the GeoLite2 database
|
||||||
|
try harness.geoip.pins.put(
|
||||||
|
try Pins.parseCidr("203.0.113.0/24"),
|
||||||
|
"Seattle, Washington, United States",
|
||||||
|
.{ .latitude = 47.6, .longitude = -122.3 },
|
||||||
|
Location.isoFrom("US"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Own cache, for the same reason as the 503 test above, holding an entry
|
||||||
|
// for the pinned coordinates that expired five minutes ago
|
||||||
|
const cache = try Cache.init(allocator, std.testing.io, .{
|
||||||
|
.cache_dir = null,
|
||||||
|
.stale_retention_s = WeatherProvider.stale_retention_s,
|
||||||
|
});
|
||||||
|
defer cache.deinit();
|
||||||
|
const now = std.Io.Timestamp.now(std.testing.io, .real).toMilliseconds();
|
||||||
|
try cache.lru.put("47.6000,-122.3000", "{}", now - 5 * 60 * 1000);
|
||||||
|
harness.opts.provider = harness.mock.provider(cache);
|
||||||
|
|
||||||
|
var ht = httpz.testing.init(.{});
|
||||||
|
defer ht.deinit();
|
||||||
|
|
||||||
|
ht.url("/203.0.113.7?format=1");
|
||||||
|
ht.param("location", "203.0.113.7");
|
||||||
|
|
||||||
|
try handleWeather(&harness.opts, ht.req, ht.res, "203.0.113.7");
|
||||||
|
|
||||||
|
try ht.expectStatus(200);
|
||||||
|
try ht.expectBody("☀️ +68°F\n");
|
||||||
|
const pr = try ht.parseResponse();
|
||||||
|
const stale_for_s = try std.fmt.parseInt(i64, pr.headers.get("X-Weather-Stale").?, 10);
|
||||||
|
try std.testing.expect(stale_for_s >= 300 and stale_for_s < 360);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "handler: fresh data has no X-Weather-Stale header" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
const MockHarness = @import("Server.zig").MockHarness;
|
||||||
|
|
||||||
|
var harness = try MockHarness.init(allocator);
|
||||||
|
defer harness.deinit();
|
||||||
|
|
||||||
|
var ht = httpz.testing.init(.{});
|
||||||
|
defer ht.deinit();
|
||||||
|
|
||||||
|
ht.url("/73.158.64.1?format=1");
|
||||||
|
ht.param("location", "73.158.64.1");
|
||||||
|
|
||||||
|
try handleWeather(&harness.opts, ht.req, ht.res, "73.158.64.1");
|
||||||
|
|
||||||
|
try ht.expectStatus(200);
|
||||||
|
const pr = try ht.parseResponse();
|
||||||
|
try std.testing.expect(pr.headers.get("X-Weather-Stale") == null);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,15 @@ pub fn fetch(self: WeatherProvider, allocator: std.mem.Allocator, coords: Coordi
|
||||||
const raw = self.vtable.fetchRaw(self.ptr, allocator, coords) catch |err| {
|
const raw = self.vtable.fetchRaw(self.ptr, allocator, coords) catch |err| {
|
||||||
if (self.cache.getStale(cache_key)) |stale| {
|
if (self.cache.getStale(cache_key)) |stale| {
|
||||||
const now_ms = std.Io.Timestamp.now(self.cache.io, .real).toMilliseconds();
|
const now_ms = std.Io.Timestamp.now(self.cache.io, .real).toMilliseconds();
|
||||||
|
const stale_for_s = @divTrunc(now_ms - stale.expires, std.time.ms_per_s);
|
||||||
log.warn("weather fetch for {s} failed ({}); serving cached data that expired {d}s ago", .{
|
log.warn("weather fetch for {s} failed ({}); serving cached data that expired {d}s ago", .{
|
||||||
cache_key,
|
cache_key,
|
||||||
err,
|
err,
|
||||||
@divTrunc(now_ms - stale.expires, std.time.ms_per_s),
|
stale_for_s,
|
||||||
});
|
});
|
||||||
return self.vtable.parse(self.ptr, allocator, stale.value);
|
var weather = try self.vtable.parse(self.ptr, allocator, stale.value);
|
||||||
|
weather.stale_for_s = stale_for_s;
|
||||||
|
return weather;
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
};
|
};
|
||||||
|
|
@ -98,6 +101,8 @@ test "provider fetch" {
|
||||||
|
|
||||||
try std.testing.expect(weather1.forecast.len > 0);
|
try std.testing.expect(weather1.forecast.len > 0);
|
||||||
try std.testing.expect(weather2.forecast.len > 0);
|
try std.testing.expect(weather2.forecast.len > 0);
|
||||||
|
try std.testing.expect(weather1.stale_for_s == null);
|
||||||
|
try std.testing.expect(weather2.stale_for_s == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "provider serves expired cached data when the upstream fails" {
|
test "provider serves expired cached data when the upstream fails" {
|
||||||
|
|
@ -129,6 +134,9 @@ test "provider serves expired cached data when the upstream fails" {
|
||||||
const weather = try provider.fetch(std.testing.allocator, coords);
|
const weather = try provider.fetch(std.testing.allocator, coords);
|
||||||
defer weather.deinit();
|
defer weather.deinit();
|
||||||
try std.testing.expect(weather.forecast.len > 0);
|
try std.testing.expect(weather.forecast.len > 0);
|
||||||
|
// Marked stale, by about the ten minutes it has been expired
|
||||||
|
const stale_for_s = weather.stale_for_s.?;
|
||||||
|
try std.testing.expect(stale_for_s >= 600 and stale_for_s < 660);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "provider does not serve data past the stale retention window" {
|
test "provider does not serve data past the stale retention window" {
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,9 @@ pub const WeatherData = struct {
|
||||||
current: CurrentCondition,
|
current: CurrentCondition,
|
||||||
forecast: []ForecastDay,
|
forecast: []ForecastDay,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
/// Set when the upstream was unavailable and this came from an expired
|
||||||
|
/// cache entry: seconds since that entry expired. Null for fresh data.
|
||||||
|
stale_for_s: ?i64 = null,
|
||||||
|
|
||||||
pub fn deinit(self: WeatherData) void {
|
pub fn deinit(self: WeatherData) void {
|
||||||
self.allocator.free(self.location);
|
self.allocator.free(self.location);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue