mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24: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
@@ -9,7 +9,10 @@ import {
|
||||
fetchOverpassPayload,
|
||||
overpassNotConfigured,
|
||||
} from './overpass/transport.js';
|
||||
import { resolveOverpassUpstreams } from './overpass/constants.js';
|
||||
import {
|
||||
resolveOverpassUpstreams,
|
||||
resolveOverpassUpstreamTag,
|
||||
} from './overpass/constants.js';
|
||||
import {
|
||||
_militaryInstallationCache,
|
||||
trimMilitaryInstallationCache,
|
||||
@@ -74,7 +77,13 @@ function militaryInstallationsProxy() {
|
||||
retrievedAt: new Date().toISOString(),
|
||||
status: 'ready',
|
||||
};
|
||||
const entry = { payload, cachedAt: Date.now() };
|
||||
// Tagged with the configured upstream set so the disk layer does not serve
|
||||
// this answer back after the operator repoints (see overpass/cache.js).
|
||||
const entry = {
|
||||
payload,
|
||||
cachedAt: Date.now(),
|
||||
upstreams: resolveOverpassUpstreamTag(),
|
||||
};
|
||||
_militaryInstallationCache.set(key, entry);
|
||||
trimMilitaryInstallationCache();
|
||||
writeMilitaryInstallationDisk(key, entry);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
MILITARY_INSTALLATION_DISK_DIR,
|
||||
MILITARY_INSTALLATION_MAX_CACHE,
|
||||
} from './constants.js';
|
||||
import { cachedByConfiguredUpstream } from '../overpass/constants.js';
|
||||
import path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { promises as fsp } from 'node:fs';
|
||||
@@ -114,6 +115,9 @@ async function readMilitaryInstallationDisk(
|
||||
const entry = JSON.parse(
|
||||
await fsp.readFile(militaryInstallationDiskPath(cacheKey, dir), 'utf8'),
|
||||
);
|
||||
// Same reason as the Overpass disk cache: the key is the bbox alone, so an
|
||||
// entry written against a different OVERPASS_UPSTREAMS is otherwise a hit.
|
||||
if (!cachedByConfiguredUpstream(entry)) return null;
|
||||
if (!militaryInstallationDiskFresh(entry, maxAgeMs)) return null;
|
||||
return migrateMilitaryInstallationEntry(entry);
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
OVERPASS_DISK_DIR,
|
||||
OVERPASS_CACHE_MS,
|
||||
OVERPASS_CACHE_MAX_ENTRIES,
|
||||
cachedByConfiguredUpstream,
|
||||
} from './constants.js';
|
||||
import path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
@@ -47,6 +48,11 @@ async function readOverpassDisk(cacheKey, maxAgeMs) {
|
||||
// Older versions persisted 4xx refusals with normal data TTLs. Ignore
|
||||
// them on both fresh and stale reads so an upgrade can recover immediately.
|
||||
if (!overpassPayloadIsData(payload)) return null;
|
||||
// The key is the query alone, so a file written by a different upstream —
|
||||
// including the public instances that were the default before #742 — is a
|
||||
// hit for the same query. Refuse it: the operator's configured instance
|
||||
// must be what answers, on the stale path too.
|
||||
if (!cachedByConfiguredUpstream(payload)) return null;
|
||||
if (Date.now() - payload.cachedAt > maxAgeMs) return null;
|
||||
return payload;
|
||||
} catch {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Overpass API proxy constants and cache state
|
||||
@@ -27,16 +28,68 @@ function parseOverpassUpstreams(raw) {
|
||||
return endpoints.slice(0, 8);
|
||||
}
|
||||
|
||||
let upstreamMemo = { raw: null, endpoints: [] };
|
||||
let upstreamMemo = { raw: null, endpoints: [], tag: 'none' };
|
||||
|
||||
/**
|
||||
* Short, stable identifier for the configured upstream set. Cached responses
|
||||
* carry it so an answer produced by a different instance is not served after
|
||||
* the operator repoints `OVERPASS_UPSTREAMS` — the disk cache outlives the
|
||||
* configuration change by 7 days, a month for boundary queries, and forever on
|
||||
* the serve-stale path. It is a hash, never the URLs, and it is only ever
|
||||
* compared on read: it is not sent to clients and does not appear in errors.
|
||||
*/
|
||||
function upstreamTag(endpoints) {
|
||||
if (!endpoints.length) return 'none';
|
||||
return createHash('sha1')
|
||||
.update(endpoints.join('\n'))
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
}
|
||||
|
||||
/** Resolve after environment loading. Public Overpass instances are not used by default. */
|
||||
function resolveOverpassUpstreams() {
|
||||
const raw = process.env.OVERPASS_UPSTREAMS || '';
|
||||
if (upstreamMemo.raw !== raw)
|
||||
upstreamMemo = { raw, endpoints: parseOverpassUpstreams(raw) };
|
||||
readUpstreamMemo();
|
||||
return [...upstreamMemo.endpoints];
|
||||
}
|
||||
|
||||
/** The configured-upstream tag stored alongside, and checked against, cache entries. */
|
||||
function resolveOverpassUpstreamTag() {
|
||||
return readUpstreamMemo().tag;
|
||||
}
|
||||
|
||||
let reportedForeignCache = false;
|
||||
|
||||
/**
|
||||
* Was this cached entry produced by the upstream set configured now?
|
||||
*
|
||||
* With nothing configured there is no instance to contradict, and serving
|
||||
* last-good data at any age is the point of the cache (the proxy's
|
||||
* not-configured path depends on it), so any entry is accepted. Once an
|
||||
* operator does configure an instance, only its own answers are served: an
|
||||
* entry with no tag, or one from a different instance, is refused rather than
|
||||
* matched on the query alone. Shared by both caches fed by these upstreams.
|
||||
*/
|
||||
function cachedByConfiguredUpstream(entry) {
|
||||
const tag = resolveOverpassUpstreamTag();
|
||||
if (tag === 'none' || entry?.upstreams === tag) return true;
|
||||
if (!reportedForeignCache) {
|
||||
reportedForeignCache = true;
|
||||
console.warn(
|
||||
'[Overpass] ignoring cached responses produced by a different OVERPASS_UPSTREAMS; they will be refetched from the configured instance.',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function readUpstreamMemo() {
|
||||
const raw = process.env.OVERPASS_UPSTREAMS || '';
|
||||
if (upstreamMemo.raw !== raw) {
|
||||
const endpoints = parseOverpassUpstreams(raw);
|
||||
upstreamMemo = { raw, endpoints, tag: upstreamTag(endpoints) };
|
||||
}
|
||||
return upstreamMemo;
|
||||
}
|
||||
|
||||
/**
|
||||
* TTL for FRESH cached Overpass responses (ms). Road geometry is static for
|
||||
* months — the original 45 s TTL forced a public-mirror round-trip on nearly
|
||||
@@ -153,6 +206,8 @@ export {
|
||||
OVERPASS_MAX_RESPONSE_BYTES,
|
||||
parseOverpassUpstreams,
|
||||
resolveOverpassUpstreams,
|
||||
resolveOverpassUpstreamTag,
|
||||
cachedByConfiguredUpstream,
|
||||
OVERPASS_USER_AGENT,
|
||||
OVERPASS_TIMEOUT_MS,
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
OVERPASS_MAX_RESPONSE_BYTES,
|
||||
resolveOverpassUpstreams,
|
||||
resolveOverpassUpstreamTag,
|
||||
OVERPASS_USER_AGENT,
|
||||
OVERPASS_TIMEOUT_MS,
|
||||
} from './constants.js';
|
||||
@@ -187,6 +188,9 @@ async function fetchOverpassPayload(
|
||||
contentType: 'application/json',
|
||||
// Never retain a secret-bearing endpoint in cache or response metadata.
|
||||
endpoint: 'configured',
|
||||
// Which configured instance produced this, as a hash rather than a URL,
|
||||
// so a cached answer is not reused after the operator repoints.
|
||||
upstreams: resolveOverpassUpstreamTag(),
|
||||
rateLimited: false,
|
||||
};
|
||||
} catch {
|
||||
|
||||
Reference in new issue
Block a user