mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge branch 'main' into upstream/traffic-layer-stale
This commit is contained in:
9 files changed
+579
-11
No files matched your search
@@ -107,7 +107,10 @@ export function terrainRetryAfterMs(value, nowMs = Date.now()) {
|
||||
/**
|
||||
* Fetch one upstream chunk with jittered exponential backoff. Network errors,
|
||||
* 429, and 5xx retry; other HTTP failures and malformed successes fail fast.
|
||||
* Retry sleeps and retry attempts share a 10s budget after attempt one.
|
||||
* Retry sleeps and retry attempts share a 10s budget after attempt one. A
|
||||
* Retry-After longer than the budget left ends the retries at once: sleeping
|
||||
* out the budget cannot reach the time the upstream asked for, and it would
|
||||
* delay the caller's own back-off (the proxy's 429 pause) by up to 10 s.
|
||||
*
|
||||
* Dependencies are injectable for deterministic offline tests.
|
||||
* @param {Array<[number, number]>} points
|
||||
@@ -145,6 +148,7 @@ export async function fetchTerrainChunkWithRetry(
|
||||
const res = await fetchImpl(url, signal ? { signal } : {});
|
||||
if (!res.ok) {
|
||||
const error = new Error(`HTTP ${res.status}`);
|
||||
error.status = res.status;
|
||||
error.retryable = res.status === 429 || res.status >= 500;
|
||||
error.retryAfter = res.headers?.get?.('retry-after') ?? null;
|
||||
throw error;
|
||||
@@ -167,6 +171,7 @@ export async function fetchTerrainChunkWithRetry(
|
||||
if (remaining <= 0) break;
|
||||
|
||||
const retryAfterMs = terrainRetryAfterMs(error?.retryAfter, now());
|
||||
if (retryAfterMs != null && retryAfterMs > remaining) break;
|
||||
const backoffMs = Math.round(
|
||||
350 * 2 ** attempt * (0.75 + random() * 0.5),
|
||||
);
|
||||
|
||||
@@ -91,6 +91,36 @@ test('429 retry honors Retry-After before the next attempt', async () => {
|
||||
assert.equal(results[0].id, 'recovered');
|
||||
});
|
||||
|
||||
test('a Retry-After longer than the retry budget ends the retries without sleeping', async () => {
|
||||
let clock = 3_000_000;
|
||||
let attempts = 0;
|
||||
const sleeps = [];
|
||||
await assert.rejects(
|
||||
fetchTerrainChunkWithRetry([[1, 1]], {
|
||||
now: () => clock,
|
||||
random: () => 0.5,
|
||||
makeSignal: () => undefined,
|
||||
sleep: async (ms) => {
|
||||
sleeps.push(ms);
|
||||
clock += ms;
|
||||
},
|
||||
fetchImpl: async () => {
|
||||
attempts += 1;
|
||||
return {
|
||||
ok: false,
|
||||
status: 429,
|
||||
headers: {
|
||||
get: (name) => (name.toLowerCase() === 'retry-after' ? '30' : null),
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
(error) => error.status === 429 && error.retryAfter === '30',
|
||||
);
|
||||
assert.equal(attempts, 1);
|
||||
assert.deepEqual(sleeps, [], 'no sleep that cannot reach the Retry-After');
|
||||
});
|
||||
|
||||
test('permanently failing upstream exhausts retries and returns 502 without fake results', async () => {
|
||||
let clock = 2_000_000;
|
||||
let attempts = 0;
|
||||
|
||||
Reference in new issue
Block a user