fix(overpass): tag caches by the sorted upstream set and cover installations

Uses a full SHA-256 of the sorted instance list, so reordering the same
instances for failover keeps the cache. Adds the installation-cache repoint
test (tagged, reordered, repointed, untagged, unconfigured) and the changelog
and current-state notes.
This commit is contained in:
Sameh Khamis committed 2026-10-15 10:48:38 -07:00
1 parent 4ede4ebbcd
commit c42b072e99
4 files changed
+39 -4

No files matched your search

+3
View File
@@ -1,5 +1,8 @@
# Changelog # Changelog
- Overpass and military-installation disk caches no longer serve answers from a
previously configured `OVERPASS_UPSTREAMS` instance after it is repointed.
- A Radio Browser mirror answering 204, 205 or 304, or a status outside - A Radio Browser mirror answering 204, 205 or 304, or a status outside
200–599, no longer crashes the local dev/preview server. Null-body statuses 200–599, no longer crashes the local dev/preview server. Null-body statuses
get an empty body; anything else that cannot become a response fails that get an empty body; anything else that cannot become a response fails that
+4
View File
@@ -4132,6 +4132,10 @@ for, resolved lazily after env loading. It replaces the empty default list;
there is no built-in fallback. Local/private instances are allowed. The stable there is no built-in fallback. Local/private instances are allowed. The stable
`gods-eye-view/…` User-Agent is unchanged. URLs never appear in response headers `gods-eye-view/…` User-Agent is unchanged. URLs never appear in response headers
or upstream errors, including old cached endpoint metadata. or upstream errors, including old cached endpoint metadata.
Disk cache entries for both routes carry a SHA-256 tag of the configured
instance set (order does not matter) and are served only while it matches, so
repointing `OVERPASS_UPSTREAMS` refetches instead of serving another instance's
answers; untagged entries stop serving once an instance is configured.
The shared transport guards `/api/overpass` and `/api/military-installations`. The shared transport guards `/api/overpass` and `/api/military-installations`.
With no configured upstream, each route serves existing cache data (including With no configured upstream, each route serves existing cache data (including
+4 -4
View File
@@ -40,10 +40,10 @@ let upstreamMemo = { raw: null, endpoints: [], tag: 'none' };
*/ */
function upstreamTag(endpoints) { function upstreamTag(endpoints) {
if (!endpoints.length) return 'none'; if (!endpoints.length) return 'none';
return createHash('sha1') // Sorted, so reordering the same instances for failover keeps the cache.
.update(endpoints.join('\n')) return createHash('sha256')
.digest('hex') .update([...endpoints].sort().join('\n'))
.slice(0, 12); .digest('hex');
} }
/** Resolve after environment loading. Public Overpass instances are not used by default. */ /** Resolve after environment loading. Public Overpass instances are not used by default. */
+28
View File
@@ -26,6 +26,7 @@ import {
validMilitaryInstallationBox, validMilitaryInstallationBox,
writeMilitaryInstallationDisk, writeMilitaryInstallationDisk,
} from '../../vite.config.js'; } from '../../vite.config.js';
import { resolveOverpassUpstreamTag } from '../../server/providers/overpass/constants.js';
const DAY_MS = 86_400_000; const DAY_MS = 86_400_000;
test('installation failure reasons disclose no raw upstream error and do not guess overload', () => { test('installation failure reasons disclose no raw upstream error and do not guess overload', () => {
@@ -274,3 +275,30 @@ test('an unwritable cache directory degrades to a miss instead of throwing', asy
await fsp.rm(dir, { recursive: true, force: true }); await fsp.rm(dir, { recursive: true, force: true });
} }
}); });
test('an installation 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 dir = tempCacheDir();
const key = '30.250,-97.750,30.300,-97.700';
try {
process.env.OVERPASS_UPSTREAMS = 'https://a.example/api/interpreter,https://b.example/api/interpreter';
const tag = resolveOverpassUpstreamTag();
await writeMilitaryInstallationDisk(key, { ...entry(Date.now()), upstreams: tag }, dir);
assert.ok(await readMilitaryInstallationDisk(key, TTL_MS, dir), 'its own entry is a hit');
process.env.OVERPASS_UPSTREAMS = 'https://b.example/api/interpreter,https://a.example/api/interpreter';
assert.equal(resolveOverpassUpstreamTag(), tag, 'reordering the same instances keeps the tag');
process.env.OVERPASS_UPSTREAMS = 'https://c.example/api/interpreter';
assert.equal(await readMilitaryInstallationDisk(key, TTL_MS, dir), null, 'fresh read refused');
assert.equal(await readMilitaryInstallationDisk(key, Infinity, dir), null, 'stale read refused');
await writeMilitaryInstallationDisk('legacy', entry(Date.now()), dir);
assert.equal(await readMilitaryInstallationDisk('legacy', Infinity, dir), null, 'untagged entry refused');
delete process.env.OVERPASS_UPSTREAMS;
assert.ok(await readMilitaryInstallationDisk(key, Infinity, dir), 'nothing configured accepts any entry');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});