mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
fix(overpass): do not serve cached answers from a different upstream (#932)
Both disk caches fed by OVERPASS_UPSTREAMS key only on the query — the Overpass proxy on the sanitized request body, the installations proxy on the bbox — so an entry written against one instance is a hit for the same query against another. After an operator repoints OVERPASS_UPSTREAMS, the previous upstream's answers keep being served for 7 days, 30 for boundary-class queries, and indefinitely through the serve-stale path. An install that predates #742 has a cache written from the public instances that were the default then, and configuring your own instance does not displace it. Cached answers now carry a tag identifying the configured upstream set, and a disk read refuses an entry that does not match it, on the stale path too. With nothing configured any entry is still accepted: there is no instance to contradict, and serving last-good data at any age is what the not-configured path exists for. The tag is a hash of the resolved endpoint list, never the URLs; it is written to disk and compared on read, and is not sent to clients or surfaced in errors. One line is logged once per process when a foreign entry is ignored, naming the variable and what will happen, as local-receivers.js already does for a rejected feed. Cost worth naming: on upgrade every existing entry is untagged, so the first outage after upgrading serves nothing where it previously served stale data, until each query is fetched once from the configured instance.
This commit is contained in:
1 parent
e1cc7afacf
commit
7485b16ded
6 files changed
+135
-7
No files matched your search
@@ -12,6 +12,7 @@ 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';
|
||||
import { resolveOverpassUpstreamTag } from '../server/providers/overpass/constants.js';
|
||||
|
||||
const DATA = { status: 200, body: '{"elements":[]}' };
|
||||
|
||||
@@ -155,7 +156,13 @@ test('coalesced outage callers both receive last-good data, never a cached refus
|
||||
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 };
|
||||
// Last-good data was fetched from the instance configured above, so it
|
||||
// carries that instance's tag the way the transport now writes it.
|
||||
const stale = {
|
||||
...DATA,
|
||||
cachedAt: Date.now() - 40 * 86400000,
|
||||
upstreams: resolveOverpassUpstreamTag(),
|
||||
};
|
||||
await writeFile(file, JSON.stringify(stale));
|
||||
const entered = Promise.withResolvers();
|
||||
const release = Promise.withResolvers();
|
||||
@@ -188,3 +195,46 @@ test('coalesced outage callers both receive last-good data, never a cached refus
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a disk entry is not served after OVERPASS_UPSTREAMS is repointed', async (t) => {
|
||||
const prior = process.env.OVERPASS_UPSTREAMS;
|
||||
t.after(() => {
|
||||
if (prior === undefined) delete process.env.OVERPASS_UPSTREAMS;
|
||||
else process.env.OVERPASS_UPSTREAMS = prior;
|
||||
});
|
||||
const key = `overpass-upstream-switch-${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 {
|
||||
// Written while one instance was configured...
|
||||
process.env.OVERPASS_UPSTREAMS = 'https://first.example/api/interpreter';
|
||||
const first = resolveOverpassUpstreamTag();
|
||||
await writeFile(
|
||||
file,
|
||||
JSON.stringify({ ...DATA, cachedAt: Date.now(), upstreams: first }),
|
||||
);
|
||||
assert.ok(await readOverpassDisk(key, 60000), 'its own entry is a hit');
|
||||
|
||||
// ...is not an answer once the operator points at a different one, on the
|
||||
// stale path either: the key is the query alone, so it would otherwise hit.
|
||||
process.env.OVERPASS_UPSTREAMS = 'https://second.example/api/interpreter';
|
||||
assert.notEqual(resolveOverpassUpstreamTag(), first, 'tag follows the env');
|
||||
assert.equal(await readOverpassDisk(key, 60000), null, 'fresh read refused');
|
||||
assert.equal(
|
||||
await readOverpassDisk(key, Infinity),
|
||||
null,
|
||||
'stale read refused',
|
||||
);
|
||||
|
||||
// With nothing configured there is no instance to contradict, and serving
|
||||
// last-good data is what the not-configured path exists for.
|
||||
delete process.env.OVERPASS_UPSTREAMS;
|
||||
assert.ok(await readOverpassDisk(key, Infinity), 'unconfigured still serves');
|
||||
} finally {
|
||||
await unlink(file);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user