mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(street-level): a tile fetch every client has left is cancelled, or never started
A client drops the tiles a camera move leaves behind; their fetches no longer run on or hold the six upstream slots ahead of the tiles in view. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
99264e80d6
commit
777aa62586
4 files changed
+203
-32
No files matched your search
@@ -14,6 +14,8 @@ import {
|
||||
TILE_MEMORY_BUDGET_BYTES,
|
||||
TILE_UPSTREAM_CONCURRENCY,
|
||||
TILE_TTL_MS,
|
||||
fetchTile,
|
||||
parseTilePath,
|
||||
_resetTileCacheForTest,
|
||||
_tileMemoryForTest,
|
||||
} from '../../server/providers/mapillary/tiles.js';
|
||||
@@ -49,13 +51,23 @@ function install(mode = 'configureServer') {
|
||||
mapillaryProxy()[mode]({
|
||||
middlewares: { use: (route, handler) => routes.set(route, handler) },
|
||||
});
|
||||
const call = async (route, url = '/', method = 'GET', reqHeaders = {}) => {
|
||||
const call = async (
|
||||
route,
|
||||
url = '/',
|
||||
method = 'GET',
|
||||
reqHeaders = {},
|
||||
onResponse = null,
|
||||
) => {
|
||||
const handler = routes.get(route);
|
||||
assert.ok(handler, `route ${route} is mounted`);
|
||||
const headers = {};
|
||||
const listeners = new Map();
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
headers,
|
||||
on: (type, listener) => listeners.set(type, listener),
|
||||
/** The client goes away, as Node reports it. */
|
||||
close: () => listeners.get('close')?.(),
|
||||
setHeader: (name, value) => (headers[name.toLowerCase()] = value),
|
||||
getHeader: (name) => headers[name.toLowerCase()],
|
||||
writeHead(status, extra) {
|
||||
@@ -67,6 +79,7 @@ function install(mode = 'configureServer') {
|
||||
this.writableEnded = true;
|
||||
},
|
||||
};
|
||||
onResponse?.(res);
|
||||
await handler({ url, method, headers: reqHeaders }, res);
|
||||
return res;
|
||||
};
|
||||
@@ -102,7 +115,7 @@ async function withUpstream(answer, run) {
|
||||
globalThis.fetch = async (url, init) => {
|
||||
calls.push(String(url));
|
||||
inits.push(init);
|
||||
return answer(calls.length, String(url));
|
||||
return answer(calls.length, String(url), init);
|
||||
};
|
||||
try {
|
||||
await withToken('MLY|test|token', () =>
|
||||
@@ -410,6 +423,101 @@ test('at most a few upstream fetches run at once; the rest wait their turn', asy
|
||||
);
|
||||
});
|
||||
|
||||
/** An upstream that answers when released, and fails as a fetch does when aborted. */
|
||||
function heldUpstream() {
|
||||
let release;
|
||||
const gate = new Promise((resolve) => (release = resolve));
|
||||
const answer = (n, url, init) =>
|
||||
new Promise((resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () =>
|
||||
reject(new DOMException('aborted', 'AbortError')),
|
||||
);
|
||||
gate.then(() => resolve(ok()));
|
||||
});
|
||||
return { answer, release };
|
||||
}
|
||||
|
||||
const settleSoon = () => new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
test(
|
||||
'a queued fetch whose client left never reaches Mapillary',
|
||||
{ timeout: 5000 },
|
||||
async () => {
|
||||
const upstream = heldUpstream();
|
||||
await withUpstream(upstream.answer, async ({ calls }) => {
|
||||
const busy = Array.from({ length: TILE_UPSTREAM_CONCURRENCY }, (_, i) =>
|
||||
fetchTile(parseTilePath(`/coverage/14/${i}/5`)),
|
||||
);
|
||||
const client = new AbortController();
|
||||
const queued = fetchTile(parseTilePath('/coverage/14/99/5'), {
|
||||
signal: client.signal,
|
||||
});
|
||||
await settleSoon();
|
||||
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY, 'the 7th waits');
|
||||
client.abort();
|
||||
await assert.rejects(queued, { name: 'AbortError' });
|
||||
upstream.release();
|
||||
await Promise.all(busy);
|
||||
await settleSoon();
|
||||
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY, 'and never ran');
|
||||
// The freed slots still serve a live request.
|
||||
await fetchTile(parseTilePath('/coverage/14/98/5'));
|
||||
assert.equal(calls.length, TILE_UPSTREAM_CONCURRENCY + 1);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'a client that disconnects cancels its tile fetch and gets no answer',
|
||||
{ timeout: 5000 },
|
||||
async () => {
|
||||
const upstream = heldUpstream();
|
||||
await withUpstream(upstream.answer, async ({ inits, call }) => {
|
||||
let res;
|
||||
const done = call(
|
||||
'/api/mapillary/tiles',
|
||||
'/coverage/14/8/8',
|
||||
'GET',
|
||||
{},
|
||||
(r) => (res = r),
|
||||
);
|
||||
await settleSoon();
|
||||
res.close();
|
||||
await done;
|
||||
assert.equal(inits[0].signal.aborted, true, 'the upstream fetch stopped');
|
||||
assert.equal(res.body, undefined, 'nothing written to a closed socket');
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'a shared fetch is cancelled only when its last client leaves',
|
||||
{ timeout: 5000 },
|
||||
async () => {
|
||||
const upstream = heldUpstream();
|
||||
await withUpstream(upstream.answer, async ({ calls, inits }) => {
|
||||
const address = parseTilePath('/coverage/14/7/7');
|
||||
const first = new AbortController();
|
||||
const second = new AbortController();
|
||||
const a = fetchTile(address, { signal: first.signal });
|
||||
const b = fetchTile(address, { signal: second.signal });
|
||||
await settleSoon();
|
||||
assert.equal(calls.length, 1, 'one fetch for both');
|
||||
first.abort();
|
||||
await assert.rejects(a, { name: 'AbortError' });
|
||||
assert.equal(inits[0].signal.aborted, false, 'the other still waits');
|
||||
second.abort();
|
||||
await assert.rejects(b, { name: 'AbortError' });
|
||||
assert.equal(inits[0].signal.aborted, true, 'the last one left');
|
||||
// A new request starts afresh rather than joining the cancelled fetch.
|
||||
upstream.release();
|
||||
const again = await fetchTile(address);
|
||||
assert.equal(again.source, 'upstream');
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('a tile held in memory past its TTL is fetched again', async (t) => {
|
||||
await withUpstream(
|
||||
() => ok(),
|
||||
|
||||
Reference in new issue
Block a user