mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(terrain): keep cold height requests within the browser deadline (#348)
Use 64-point browser and upstream terrain batches to reduce cold-cache timeouts. Preserve successful chunk results, distinguish absent readings from malformed responses, and reject fabricated zero heights. Original terrain resilience contribution by ItzDESTAQ, with browser batching, validation and regression follow-up by Sameh Khamis. Validated with unit/build/format/boundary checks, 108 tracking checks, 5 terrain-outage checks, controlled slow-upstream comparison and live browser checks. Co-authored-by: ItzDESTAQ <[email protected]> Co-authored-by: Sameh Khamis <[email protected]>
This commit is contained in:
1 parent
4e99f40488
commit
db49edf363
8 files changed
+254
-32
No files matched your search
@@ -12,18 +12,27 @@ import {
|
||||
/**
|
||||
* Re:Earth terrain point-height proxy: batched lon/lat → ellipsoidal height
|
||||
* lookups, keyless. Upstream: https://terrain.reearth.land/heights.json
|
||||
* (≤256 points per call). Terrain doesn't move, so results are cached to
|
||||
* (UPSTREAM_CHUNK points per call — sized against measured latency, see
|
||||
* below). Terrain doesn't move, so results are cached to
|
||||
* disk with a long TTL (30 days) — mirrors celestrakProxy's memory+disk
|
||||
* cache and serve-stale shape. Cache entries and stale fallback are keyed per
|
||||
* 5dp point, so reordered and partially overlapping batches reuse prior work.
|
||||
* Only missing/stale points go upstream; the response is rebuilt in exact
|
||||
* request order. Oversized requests (>256 points) are chunked sequentially.
|
||||
* request order. Larger requests are chunked sequentially, and one failing
|
||||
* chunk does not discard the chunks that resolved.
|
||||
*/
|
||||
export function terrainHeightsProxy() {
|
||||
const TTL_MS = 30 * 24 * 3600_000;
|
||||
const CACHE_DIR = path.join(process.cwd(), '.gev-cache');
|
||||
const CACHE_PATH = path.join(CACHE_DIR, 'terrain-heights.json');
|
||||
const UPSTREAM_CHUNK = 256;
|
||||
// Sized against measured upstream latency, not the documented page cap.
|
||||
// Re:Earth serves 87-186 ms/point depending on load (observed 2026-09-12,
|
||||
// a 2x swing within one hour). 256 points therefore costs 22-48 s and blows
|
||||
// the 30 s attempt timeout whenever the service is slow — the long-standing
|
||||
// cause of "[terrain-heights-proxy] refresh incomplete" (owner incident
|
||||
// 2026-08-21). 64 points costs 5.6-12 s, inside the timeout at both ends of
|
||||
// that range.
|
||||
const UPSTREAM_CHUNK = 64;
|
||||
const MAX_POINTS = 2000;
|
||||
|
||||
/** @type {Map<string, {at:number, result:object}>} keyed by canonical 5dp lon/lat. */
|
||||
@@ -98,24 +107,39 @@ export function terrainHeightsProxy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all missing chunks sequentially (upstream caps each call at 256).
|
||||
* Fetch all missing chunks sequentially, UPSTREAM_CHUNK points per call.
|
||||
*
|
||||
* The first try keeps its empirically required 30s timeout; network errors,
|
||||
* 429, and 5xx receive up to three jittered retries sharing a 10s added-time
|
||||
* budget, with Retry-After honored within that bound.
|
||||
*
|
||||
* A chunk that still fails yields nulls for its own positions rather than
|
||||
* rejecting: with several chunks per request, throwing would discard every
|
||||
* chunk that did resolve and re-fetch it on the next poll. The resolver
|
||||
* caches the resolved positions and reports the null ones as omitted. Total
|
||||
* failure (nothing resolved) still rejects, so the real upstream error
|
||||
* reaches the caller instead of a generic omission count.
|
||||
* @param {Array<[number, number]>} points
|
||||
* @returns {Promise<Array<object>>}
|
||||
*/
|
||||
async function fetchUpstreamAll(points) {
|
||||
const results = [];
|
||||
let firstError = null;
|
||||
for (let i = 0; i < points.length; i += UPSTREAM_CHUNK) {
|
||||
const chunk = points.slice(i, i + UPSTREAM_CHUNK);
|
||||
const chunkResults = await fetchTerrainChunkWithRetry(chunk);
|
||||
let chunkResults = [];
|
||||
try {
|
||||
chunkResults = await fetchTerrainChunkWithRetry(chunk);
|
||||
} catch (error) {
|
||||
firstError = firstError || error;
|
||||
}
|
||||
// Keep later chunks aligned even if a malformed upstream response omits
|
||||
// trailing positions. The resolver will reject each null individually.
|
||||
for (let j = 0; j < chunk.length; j += 1)
|
||||
results.push(chunkResults[j] ?? null);
|
||||
}
|
||||
if (firstError && results.every((result) => result == null))
|
||||
throw firstError;
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -169,6 +193,16 @@ export function terrainHeightsProxy() {
|
||||
'[terrain-heights-proxy] refresh incomplete' +
|
||||
' — serving stale points when available',
|
||||
);
|
||||
} else if (outcome.absentPoints > 0) {
|
||||
// Not a refresh failure. The upstream answered every position and
|
||||
// returned a null height for a few of them (~0.16%, transient);
|
||||
// the next poll re-asks and the client meanwhile resolves those
|
||||
// through its bundled geoid. Informational, not actionable.
|
||||
console.info(
|
||||
`[terrain-heights-proxy] ${outcome.absentPoints}` +
|
||||
` of ${outcome.requestedPoints} position(s) had no upstream` +
|
||||
' height this poll — retrying next cycle',
|
||||
);
|
||||
}
|
||||
send(outcome.status, outcome.body);
|
||||
} catch (err) {
|
||||
|
||||
Reference in new issue
Block a user