fix(weather): queue RainViewer tiles briefly instead of failing a burst

This commit is contained in:
Bilawal Sidhu committed 2026-09-28 20:46:58 -05:00
1 parent 9f3cdf8203
commit 9c2fce4254
5 files changed
+250 -8

No files matched your search

+91 -3
View File
@@ -1,12 +1,100 @@
import { setTimeout as sleepDefault } from 'node:timers/promises';
/** Admit at most 80 requests in a rolling minute; return retry seconds or zero.
* Call only at the upstream boundary, after cache lookup and coalescing. */
export function createRainViewerGovernor() {
const attempts = [];
return (now) => {
const queue = [];
let timer;
const delay = (now) => {
while (attempts.length && now - attempts[0] >= 60_000) attempts.shift();
if (attempts.length >= 80)
return Math.max(1, Math.ceil((60_000 - (now - attempts[0])) / 1000));
return attempts.length >= 80 ? 60_000 - (now - attempts[0]) : 0;
};
const retry = (now) => Math.max(1, Math.ceil(delay(now) / 1000));
const check = (now) => {
if (delay(now) || queue.length) return retry(now);
attempts.push(now);
return 0;
};
function finish(waiter, value, error) {
queue.splice(queue.indexOf(waiter), 1);
waiter.signal?.removeEventListener('abort', waiter.abort);
if (error) waiter.reject(error);
else waiter.resolve(value);
}
function pump() {
timer?.abort();
timer = null;
for (const waiter of [...queue]) {
const time = waiter.now();
if (time > waiter.deadline || delay(time) > waiter.deadline - time)
finish(waiter, retry(time));
}
while (queue.length) {
const waiter = queue[0];
const time = waiter.now();
const wait = delay(time);
if (wait > waiter.deadline - time) {
finish(waiter, retry(time));
continue;
}
if (wait) {
const controller = new AbortController();
timer = controller;
const duration = Math.min(
wait,
...queue.map((entry) => entry.deadline - entry.now()),
);
Promise.resolve()
.then(() =>
waiter.sleep(duration, undefined, { signal: controller.signal }),
)
.then(
() => {
if (timer === controller) pump();
},
(error) => {
if (timer !== controller) return;
timer = null;
for (const entry of [...queue]) finish(entry, undefined, error);
},
);
return;
}
attempts.push(time);
finish(waiter, 0);
}
}
// Pass a clock function so each wake-up checks the current rolling window.
check.admit = async (
now = Date.now,
{ signal, maxWaitMs = 20_000, maxQueued = 256, sleep = sleepDefault } = {},
) => {
signal?.throwIfAborted();
const time = now();
if (!queue.length && !delay(time)) {
attempts.push(time);
return 0;
}
if (queue.length >= maxQueued || delay(time) > maxWaitMs)
return retry(time);
return new Promise((resolve, reject) => {
const waiter = {
now,
sleep,
signal,
deadline: time + maxWaitMs,
resolve,
reject,
};
waiter.abort = () => {
finish(waiter, undefined, signal.reason);
pump();
};
queue.push(waiter);
signal?.addEventListener('abort', waiter.abort, { once: true });
pump();
});
};
return check;
}
+13 -4
View File
@@ -308,6 +308,7 @@ export function weatherProxy({
fetchImpl = fetch,
now = () => Date.now(),
timeoutMs = 12_000,
rainViewerSleep,
} = {}) {
const admitRainViewerTile = createRainViewerGovernor();
const metadata = new Map();
@@ -334,7 +335,7 @@ export function weatherProxy({
});
}
}
async function shared(key, work, clientSignal) {
async function shared(key, work, clientSignal, extraWaitMs = 0) {
clientSignal.throwIfAborted();
let operation = operations.get(key);
if (operation?.controller.signal.aborted) {
@@ -356,7 +357,10 @@ export function weatherProxy({
operation.reject(failure('weather_request_cancelled'));
};
controller.signal.addEventListener('abort', cancel, { once: true });
const timer = setTimeout(() => controller.abort(), timeoutMs);
const timer = setTimeout(
() => controller.abort(),
timeoutMs + extraWaitMs,
);
operation.promise = operation.promise.finally(() => {
clearTimeout(timer);
controller.signal.removeEventListener('abort', cancel);
@@ -659,17 +663,22 @@ export function weatherProxy({
try {
const bytes = await shared(
`image:${key}`,
(signal) => {
async (signal) => {
if (globalRadar) {
const retryAfter = admitRainViewerTile(now());
const retryAfter = await admitRainViewerTile.admit(now, {
signal,
sleep: rainViewerSleep,
});
if (retryAfter)
throw Object.assign(failure('weather_busy', 429), {
retryAfter,
});
}
signal.throwIfAborted();
return upstream(upstreamUrl.href, signal, imageShape);
},
controller.signal,
globalRadar ? 20_000 : 0,
);
rememberTile(key, bytes);
cached = { bytes };