mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(wildlife): treat a valid empty Acartia snapshot as a fresh success
A quiet or fully filtered feed raised invalid_snapshot, so the proxy answered 502 with a 15 s cooldown and re-polled about four times a minute. Only a non-array payload is malformed now; an empty projection caches for the normal ten minutes and clears earlier rows.
This commit is contained in:
1 parent
bd99000433
commit
82138478d0
3 files changed
+68
-25
No files matched your search
@@ -123,7 +123,10 @@ known `Retry-After`. Failures keep filters and drawn rows, reporting DEGRADED
|
|||||||
with retry timing; logs exclude request URLs and upstream exception text.
|
with retry timing; logs exclude request URLs and upstream exception text.
|
||||||
|
|
||||||
`/api/acartia/sightings` adds Salish Sea marine mammals within 45.5–50.5° N,
|
`/api/acartia/sightings` adds Salish Sea marine mammals within 45.5–50.5° N,
|
||||||
126–121° W, cached ten minutes and stale for at most 24 h. WHALES accepts only
|
126–121° W, cached ten minutes and stale for at most 24 h. A valid empty
|
||||||
|
snapshot, including one whose rows all fall outside coverage or the species
|
||||||
|
checks, is a fresh success that clears earlier rows; only a malformed or failed
|
||||||
|
upstream reply serves stale and starts a retry cooldown. WHALES accepts only
|
||||||
recognized cetaceans; other identified marine mammals appear under ALL or
|
recognized cetaceans; other identified marine mammals appear under ALL or
|
||||||
MAMMALS. Sensitive iNaturalist locations remain 0.2° privacy cells; Acartia
|
MAMMALS. Sensitive iNaturalist locations remain 0.2° privacy cells; Acartia
|
||||||
positions are rounded to 0.01° on the server. Acartia comments are never retained.
|
positions are rounded to 0.01° on the server. Acartia comments are never retained.
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ export function acartiaProxy({
|
|||||||
signal,
|
signal,
|
||||||
);
|
);
|
||||||
const rows = normalizeAcartiaSightings(payload, now());
|
const rows = normalizeAcartiaSightings(payload, now());
|
||||||
if (!rows?.length) throw new Error('invalid_snapshot');
|
// Only a non-array is malformed. A quiet feed, or one whose rows all fall
|
||||||
|
// outside coverage or the species checks, is a valid empty snapshot.
|
||||||
|
if (!rows) throw new Error('invalid_snapshot');
|
||||||
return { fetchedAt: now(), rows };
|
return { fetchedAt: now(), rows };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status !== 429) coolDown(502, FAILURE_COOLDOWN_MS);
|
if (error.status !== 429) coolDown(502, FAILURE_COOLDOWN_MS);
|
||||||
|
|||||||
@@ -221,43 +221,81 @@ test('one global snapshot single-flights and stays fresh for ten minutes', async
|
|||||||
assert.equal(calls, 2);
|
assert.equal(calls, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('empty, unprojectable and invented wrapped snapshots never replace the cache', async () => {
|
test('a valid empty or fully filtered snapshot is a fresh success that clears old rows', async () => {
|
||||||
for (const bad of [
|
for (const quiet of [
|
||||||
[],
|
[],
|
||||||
[null],
|
[null],
|
||||||
[{ ...sighting(), data_source_name: '' }],
|
[{ ...sighting(), data_source_name: '' }],
|
||||||
{ results: [sighting()] },
|
[{ ...sighting(), latitude: 37.8 }],
|
||||||
|
[{ ...sighting(), type: 'Unknown' }],
|
||||||
]) {
|
]) {
|
||||||
let at = NOW;
|
let at = NOW;
|
||||||
let fail = false;
|
let calls = 0;
|
||||||
|
let empty = false;
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
const request = install({
|
const request = install({
|
||||||
cache,
|
cache,
|
||||||
now: () => at,
|
now: () => at,
|
||||||
fetchImpl: async () => Response.json(fail ? bad : [sighting()]),
|
fetchImpl: async () => {
|
||||||
|
calls += 1;
|
||||||
|
return Response.json(empty ? quiet : [sighting()]);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const fresh = await request();
|
assert.equal((await request()).body.rows.length, 1);
|
||||||
const retained = cache.get('sightings');
|
empty = true;
|
||||||
fail = true;
|
|
||||||
at += 600_001;
|
at += 600_001;
|
||||||
const stale = await request();
|
const result = await request();
|
||||||
assert.equal(stale.status, 200);
|
assert.equal(result.status, 200);
|
||||||
assert.equal(stale.headers['X-Data-Stale'], 'true');
|
assert.deepEqual(result.body, { fetchedAt: at, rows: [] });
|
||||||
assert.equal(stale.body.stale, true);
|
assert.equal(result.headers['X-Data-Stale'], undefined);
|
||||||
assert.equal(stale.body.fetchedAt, fresh.body.fetchedAt);
|
assert.equal(result.headers['Retry-After'], undefined);
|
||||||
assert.equal(cache.get('sightings'), retained);
|
assert.deepEqual(cache.get('sightings').value.rows, []);
|
||||||
const coldCache = new Map();
|
// The quiet feed keeps the normal ten-minute cadence, not a failure retry.
|
||||||
const cold = install({
|
at += 599_999;
|
||||||
cache: coldCache,
|
assert.equal((await request()).status, 200);
|
||||||
fetchImpl: async () => Response.json(bad),
|
assert.equal(calls, 2);
|
||||||
});
|
at += 1;
|
||||||
const result = await cold();
|
await request();
|
||||||
assert.equal(result.status, 502);
|
assert.equal(calls, 3);
|
||||||
assert.equal(result.body.error, 'acartia_unavailable');
|
const cold = install({ fetchImpl: async () => Response.json(quiet) });
|
||||||
assert.equal(coldCache.size, 0);
|
const coldResult = await cold();
|
||||||
|
assert.equal(coldResult.status, 200);
|
||||||
|
assert.deepEqual(coldResult.body, { fetchedAt: NOW, rows: [] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an invented wrapped snapshot never replaces the cache', async () => {
|
||||||
|
const bad = { results: [sighting()] };
|
||||||
|
let at = NOW;
|
||||||
|
let fail = false;
|
||||||
|
const cache = new Map();
|
||||||
|
const request = install({
|
||||||
|
cache,
|
||||||
|
now: () => at,
|
||||||
|
fetchImpl: async () => Response.json(fail ? bad : [sighting()]),
|
||||||
|
});
|
||||||
|
const fresh = await request();
|
||||||
|
const retained = cache.get('sightings');
|
||||||
|
fail = true;
|
||||||
|
at += 600_001;
|
||||||
|
const stale = await request();
|
||||||
|
assert.equal(stale.status, 200);
|
||||||
|
assert.equal(stale.headers['X-Data-Stale'], 'true');
|
||||||
|
assert.equal(stale.body.stale, true);
|
||||||
|
assert.equal(stale.body.fetchedAt, fresh.body.fetchedAt);
|
||||||
|
assert.equal(cache.get('sightings'), retained);
|
||||||
|
const coldCache = new Map();
|
||||||
|
const cold = install({
|
||||||
|
cache: coldCache,
|
||||||
|
fetchImpl: async () => Response.json(bad),
|
||||||
|
});
|
||||||
|
const result = await cold();
|
||||||
|
assert.equal(result.status, 502);
|
||||||
|
assert.equal(result.body.error, 'acartia_unavailable');
|
||||||
|
assert.equal(result.headers['Retry-After'], '15');
|
||||||
|
assert.equal(coldCache.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('failure serves stale for at most 24 hours and never reports a cold success', async () => {
|
test('failure serves stale for at most 24 hours and never reports a cold success', async () => {
|
||||||
let at = NOW;
|
let at = NOW;
|
||||||
let fail = false;
|
let fail = false;
|
||||||
|
|||||||
Reference in new issue
Block a user