mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
fix(overpass): reject legacy cached refusals and share stale recovery
This commit is contained in:
1 parent
973b4e147f
commit
7ffa421932
5 files changed
+171
-35
No files matched your search
@@ -16,9 +16,9 @@ import {
|
||||
|
||||
test('preflight checks memory, in-flight, then disk before consuming limiter quota', async () => {
|
||||
const key = 'normalized query';
|
||||
const fresh = { id: 'memory', cachedAt: 900 };
|
||||
const joined = { id: 'inflight', cachedAt: 950 };
|
||||
const disk = { id: 'disk', cachedAt: 975 };
|
||||
const fresh = { id: 'memory', status: 200, cachedAt: 900 };
|
||||
const joined = { id: 'inflight', status: 200, cachedAt: 950 };
|
||||
const disk = { id: 'disk', status: 200, cachedAt: 975 };
|
||||
let diskReads = 0;
|
||||
let limiterCalls = 0;
|
||||
const allowUpstream = () => { limiterCalls += 1; return true; };
|
||||
@@ -39,7 +39,7 @@ test('preflight checks memory, in-flight, then disk before consuming limiter quo
|
||||
|
||||
const inFlightHit = await resolveOverpassPreflight({
|
||||
cacheKey: key,
|
||||
memoryCache: new Map([[key, { id: 'stale', cachedAt: 0 }]]),
|
||||
memoryCache: new Map([[key, { id: 'stale', status: 200, cachedAt: 0 }]]),
|
||||
inFlight: new Map([[key, Promise.resolve(joined)]]),
|
||||
readDisk: async () => { diskReads += 1; return disk; },
|
||||
allowUpstream,
|
||||
@@ -84,6 +84,21 @@ test('preflight checks memory, in-flight, then disk before consuming limiter quo
|
||||
assert.equal(denied.source, 'RATE_LIMITED');
|
||||
});
|
||||
|
||||
test('preflight treats cached refusals as misses without spending extra quota', async () => {
|
||||
for (const invalid of [{ status: 406 }, { status: 200, runtimeError: true }]) {
|
||||
let admissions = 0;
|
||||
const result = await resolveOverpassPreflight({
|
||||
cacheKey: 'refused',
|
||||
memoryCache: new Map([['refused', { ...invalid, cachedAt: Date.now() }]]),
|
||||
inFlight: new Map(),
|
||||
readDisk: async () => ({ ...invalid, cachedAt: Date.now() }),
|
||||
allowUpstream: () => { admissions++; return true; },
|
||||
});
|
||||
assert.equal(result.source, 'UPSTREAM');
|
||||
assert.equal(admissions, 1);
|
||||
}
|
||||
});
|
||||
|
||||
/** Synthetic dense ring: N points on a circle with sub-tolerance jitter. */
|
||||
function denseRing(n, { latC = 37.5, lonC = 14.2, radiusDeg = 0.5 } = {}) {
|
||||
const pts = [];
|
||||
|
||||
+115
-13
@@ -1,22 +1,17 @@
|
||||
// OVERPASS PROXY — which upstream answers count as an answer.
|
||||
//
|
||||
// The proxy fans out across four public mirrors. What decides whether a mirror
|
||||
// has answered is one predicate, and it is used TWICE: once to decide what
|
||||
// enters the cache, and once to decide when a stale entry may stand in. When
|
||||
// those two drifted, a refusal was written to memory and disk AND served as
|
||||
// data — and boundary-class queries hold a month-long TTL, so a single refusal
|
||||
// outlived the outage that caused it by weeks.
|
||||
//
|
||||
// The refusal was real and measured: overpass-api.de and its lz4 alias answer
|
||||
// 406 to this proxy's User-Agent, while kumi.systems and private.coffee answer
|
||||
// 200 to the identical request. Only 5xx rotated mirrors, so the fan-out
|
||||
// stopped at the first refusal with two healthy mirrors untried, and four of
|
||||
// twenty-three cached entries on this machine held an Apache error page.
|
||||
// One predicate governs cache reads, writes, and stale fallback. A mirror's
|
||||
// refusal must neither end the search for healthy alternatives nor persist as
|
||||
// data under the week/month-long cache TTLs. These cases use no live providers.
|
||||
//
|
||||
// Run with: npm test
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { fetchOverpassPayload, overpassPayloadIsData } from '../vite.config.js';
|
||||
import { mkdir, readFile, writeFile, unlink } from 'node:fs/promises';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import createViteConfig, { fetchOverpassPayload, overpassPayloadIsData, readOverpassDisk } from '../vite.config.js';
|
||||
|
||||
const ENDPOINTS = ['https://a.example/api', 'https://b.example/api', 'https://c.example/api'];
|
||||
|
||||
@@ -44,6 +39,31 @@ const run = (byUrl) => {
|
||||
|
||||
const DATA = { status: 200, body: '{"elements":[]}' };
|
||||
|
||||
test('disk cache rejects old refusals for fresh and stale reads but preserves last-good data', async () => {
|
||||
const key = `overpass-cache-regression-${randomUUID()}`;
|
||||
const directory = path.join(process.cwd(), '.gev-cache', 'overpass');
|
||||
const file = path.join(directory, `${createHash('sha1').update(key).digest('hex')}.json`);
|
||||
await mkdir(directory, { recursive: true });
|
||||
try {
|
||||
for (const refusal of [
|
||||
{ status: 406 }, { status: 429 }, { status: 503 },
|
||||
{ status: 200, rateLimited: true }, { status: 200, runtimeError: true },
|
||||
]) {
|
||||
await writeFile(file, JSON.stringify({ ...DATA, cachedAt: Date.now(), ...refusal }));
|
||||
assert.equal(await readOverpassDisk(key, 60000), null, `fresh ${JSON.stringify(refusal)}`);
|
||||
assert.equal(await readOverpassDisk(key, Infinity), null, `stale ${JSON.stringify(refusal)}`);
|
||||
}
|
||||
const good = { ...DATA, cachedAt: Date.now() - 120000 };
|
||||
await writeFile(file, JSON.stringify(good));
|
||||
assert.equal(await readOverpassDisk(key, 60000), null, 'expired good data misses normal TTL');
|
||||
assert.deepEqual(await readOverpassDisk(key, Infinity), good, 'last-good data survives an outage');
|
||||
await writeFile(file, '{invalid');
|
||||
assert.equal(await readOverpassDisk(key, Infinity), null, 'corrupt cache is ignored');
|
||||
} finally {
|
||||
await unlink(file);
|
||||
}
|
||||
});
|
||||
|
||||
// ── The predicate ────────────────────────────────────────────────────────────
|
||||
|
||||
test('only a 2xx that is neither rate-limited nor a runtime error is data', () => {
|
||||
@@ -121,3 +141,85 @@ test('when every mirror is unreachable the caller gets a throw, not a payload',
|
||||
assert.equal(payload, undefined);
|
||||
assert.match(error.message, /ENOTFOUND/);
|
||||
});
|
||||
|
||||
test('production reader rotates past oversized, runtime-error and rate-limited bodies', async () => {
|
||||
for (const [status, body] of [
|
||||
[200, 'x'.repeat(200)], [200, '{"remark":"runtime error: timed out","elements":[]}'],
|
||||
[200, 'rate_limited'], [429, 'busy'], [403, 'forbidden'],
|
||||
]) {
|
||||
const tried = [];
|
||||
const payload = await fetchOverpassPayload('data=x', 100, {
|
||||
endpoints: ENDPOINTS,
|
||||
fetchImpl: async (url) => {
|
||||
tried.push(url);
|
||||
return new Response(tried.length === 1 ? body : DATA.body, {
|
||||
status: tried.length === 1 ? status : 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(payload.body, DATA.body);
|
||||
assert.deepEqual(tried, ENDPOINTS.slice(0, 2));
|
||||
}
|
||||
});
|
||||
|
||||
function proxyHandler() {
|
||||
const plugin = createViteConfig({ mode: 'test' }).plugins.find(p => p.name === 'overpass-proxy');
|
||||
const routes = new Map();
|
||||
plugin.configureServer({ middlewares: { use: (route, handler) => routes.set(route, handler) } });
|
||||
return routes.get('/api/overpass');
|
||||
}
|
||||
|
||||
function invoke(handler, body) {
|
||||
const req = Readable.from([Buffer.from(body)]);
|
||||
Object.assign(req, { method: 'POST', headers: {}, socket: { remoteAddress: '127.0.0.1' } });
|
||||
return new Promise((resolve, reject) => {
|
||||
const res = {
|
||||
writeHead(status, headers) { this.status = status; this.headers = headers; },
|
||||
end(body) { resolve({ status: this.status, headers: this.headers, body }); },
|
||||
};
|
||||
Promise.resolve(handler(req, res)).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
test('coalesced outage callers both receive last-good data, never a cached refusal', async (t) => {
|
||||
const handler = proxyHandler();
|
||||
for (const status of [406, 503, 429]) {
|
||||
const query = `[out:json][timeout:12];node(around:10,30.27,-97.74)["name"="${randomUUID()}"];out;`;
|
||||
const body = `data=${encodeURIComponent(query)}`;
|
||||
const directory = path.join(process.cwd(), '.gev-cache', 'overpass');
|
||||
const file = path.join(directory, `${createHash('sha1').update(body).digest('hex')}.json`);
|
||||
await mkdir(directory, { recursive: true });
|
||||
const stale = { ...DATA, cachedAt: Date.now() - 40 * 86400000 };
|
||||
await writeFile(file, JSON.stringify(stale));
|
||||
const entered = Promise.withResolvers();
|
||||
const release = Promise.withResolvers();
|
||||
let fetches = 0;
|
||||
const mock = t.mock.method(globalThis, 'fetch', async () => {
|
||||
fetches++;
|
||||
entered.resolve();
|
||||
await release.promise;
|
||||
return new Response('upstream unavailable', { status });
|
||||
});
|
||||
try {
|
||||
const first = invoke(handler, body);
|
||||
await entered.promise;
|
||||
const second = invoke(handler, body);
|
||||
// The second request consumes its in-memory stream and joins the pending
|
||||
// promise before releasing upstream. No network or elapsed-time sleep.
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
release.resolve();
|
||||
for (const response of await Promise.all([first, second])) {
|
||||
assert.equal(response.status, 200, `${status}: both callers use last-good data`);
|
||||
assert.equal(response.body, DATA.body);
|
||||
assert.equal(response.headers['X-Overpass-Cache'], 'STALE');
|
||||
}
|
||||
assert.equal(fetches, 4, 'one shared, bounded mirror sequence');
|
||||
assert.deepEqual(JSON.parse(await readFile(file, 'utf8')), stale);
|
||||
} finally {
|
||||
release.resolve();
|
||||
mock.mock.restore();
|
||||
await unlink(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user