mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Bound fallback retry attempts with a deadline
An Esri or keyless terrain attempt that never settled kept the single-flight retry open forever, so neither provider was retried again. Each attempt now counts as failed after 60 seconds and the retry backs off as usual; its signal is aborted, and a provider that lands after the deadline is released rather than installed or cached. CURRENT-STATE now says only the construction fallback to OSM is retried, not the switch made after repeated Esri tile failures.
This commit is contained in:
1 parent
f0d43a62e8
commit
759c7964c7
3 files changed
+110
-9
No files matched your search
+29
-4
@@ -12,6 +12,8 @@ const AUTOMATIC = Symbol('automatic switch');
|
||||
/** Background retries of a fallback in use back off from 30 s to 10 min. */
|
||||
const FALLBACK_RETRY_MS = 30_000;
|
||||
const FALLBACK_RETRY_MAX_MS = 600_000;
|
||||
/** A retry attempt still unsettled after a minute counts as failed. */
|
||||
const FALLBACK_ATTEMPT_MS = 60_000;
|
||||
|
||||
const resolved = (provider, effectiveStackId) => ({
|
||||
provider,
|
||||
@@ -354,17 +356,38 @@ export class MapSourceController {
|
||||
if (!id && !terrain) return;
|
||||
this._retrying = true;
|
||||
this._retryRequested = false;
|
||||
const attempt = async (create) => {
|
||||
// An attempt that outlives its deadline counts as a failure. Its signal is
|
||||
// aborted, but the Esri and keyless factories ignore it and Cesium's
|
||||
// requests have no timeout, so a late result is released, never used.
|
||||
const attempt = async (create, release) => {
|
||||
const expiry = new AbortController();
|
||||
let timer;
|
||||
const pending = Promise.resolve().then(() =>
|
||||
create?.({
|
||||
signal: AbortSignal.any([this._abort.signal, expiry.signal]),
|
||||
}),
|
||||
);
|
||||
const deadline = new Promise((resolve) => {
|
||||
timer = setTimeout(resolve, FALLBACK_ATTEMPT_MS, expiry);
|
||||
});
|
||||
try {
|
||||
return await create?.({ signal: this._abort.signal });
|
||||
const result = await Promise.race([pending, deadline]);
|
||||
if (result !== expiry) return result;
|
||||
expiry.abort();
|
||||
pending.then(release, () => {});
|
||||
return null;
|
||||
} catch {
|
||||
return null; // Still unavailable: keep the fallback.
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
// Each recovers as soon as it loads, never waiting on the other.
|
||||
const retryImagery = async () => {
|
||||
const cached = this._imageryProviders.get(id);
|
||||
const provider = await attempt(this._sources.get(id).imagery);
|
||||
const provider = await attempt(this._sources.get(id).imagery, (late) =>
|
||||
this._dispose(late),
|
||||
);
|
||||
// Obsolete: destroyed, or the cache moved on while this was in flight.
|
||||
if (this._destroyed || this._imageryProviders.get(id) !== cached) {
|
||||
this._dispose(provider);
|
||||
@@ -388,7 +411,9 @@ export class MapSourceController {
|
||||
};
|
||||
const retryTerrain = async () => {
|
||||
const cached = this._terrainProviders.get(terrain.id);
|
||||
const real = await attempt(terrain.create);
|
||||
const real = await attempt(terrain.create, (late) =>
|
||||
this._dispose(late?.provider),
|
||||
);
|
||||
if (
|
||||
this._destroyed ||
|
||||
this._terrainProviders.get(terrain.id) !== cached ||
|
||||
|
||||
@@ -1180,3 +1180,79 @@ test('a stalled imagery retry does not hold back recovered terrain', async (t) =
|
||||
await settle();
|
||||
env.controller.destroy();
|
||||
});
|
||||
|
||||
test('a retry attempt that never settles is abandoned at its deadline and released if it lands late', async (t) => {
|
||||
const hung = deferred();
|
||||
const late = { id: 'late', destroy: () => (late.destroyed = true) };
|
||||
const { env, installed, terrain } = terrainFixture(t, async (n) => {
|
||||
if (n === 2) await hung.promise;
|
||||
return n === 2 ? { provider: late } : flatTerrain(n);
|
||||
});
|
||||
const esri = failingEsri(env);
|
||||
await env.controller.setStack('esri-imagery');
|
||||
t.mock.timers.tick(30_000);
|
||||
await settle();
|
||||
assert.equal(esri.attempts, 2, 'imagery failed');
|
||||
assert.equal(terrain.calls, 2, 'terrain hangs');
|
||||
t.mock.timers.tick(60_000); // The hung attempt's deadline.
|
||||
await settle();
|
||||
t.mock.timers.tick(60_000); // The doubled backoff.
|
||||
await settle();
|
||||
assert.equal(esri.attempts, 3, 'imagery is retried again');
|
||||
assert.equal(terrain.calls, 3, 'terrain is retried again');
|
||||
hung.resolve();
|
||||
await settle();
|
||||
assert.equal(late.destroyed, true, 'the late provider is released');
|
||||
assert.ok(!installed.includes(late), 'and never installed');
|
||||
env.controller.destroy();
|
||||
});
|
||||
|
||||
test('a failed retry after a mid-retry re-show re-arms once, after the doubled delay', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const env = publicFixture();
|
||||
const esri = failingEsri(env);
|
||||
await env.controller.setStack('esri-imagery');
|
||||
const gate = deferred();
|
||||
esri.next = gate.promise;
|
||||
t.mock.timers.tick(30_000);
|
||||
await settle();
|
||||
await env.controller.setStack('osm');
|
||||
await env.controller.setStack('esri-imagery');
|
||||
gate.resolve();
|
||||
await settle();
|
||||
assert.equal(esri.attempts, 2);
|
||||
t.mock.timers.tick(59_999);
|
||||
await settle();
|
||||
assert.equal(esri.attempts, 2, 'not before the doubled delay');
|
||||
t.mock.timers.tick(1);
|
||||
await settle();
|
||||
assert.equal(esri.attempts, 3, 'one retry after it');
|
||||
t.mock.timers.tick(119_999);
|
||||
await settle();
|
||||
assert.equal(esri.attempts, 3, 'no second timer was armed');
|
||||
env.controller.destroy();
|
||||
});
|
||||
|
||||
test('destroy releases recovered imagery and terrain that lands afterwards', async (t) => {
|
||||
const stalled = deferred();
|
||||
const real = { id: 'real', destroy: () => (real.destroyed = true) };
|
||||
const { env, installed } = terrainFixture(t, async (n) => {
|
||||
if (n === 1) return flatTerrain(n);
|
||||
await stalled.promise;
|
||||
return { provider: real };
|
||||
});
|
||||
const esri = failingEsri(env);
|
||||
const imagery = env.providers.get('esri-imagery');
|
||||
imagery.destroy = () => (imagery.destroyed = true);
|
||||
await env.controller.setStack('esri-imagery');
|
||||
esri.failing = false;
|
||||
t.mock.timers.tick(30_000);
|
||||
await settle();
|
||||
assert.equal(env.imagery.at(-1).provider, imagery, 'imagery has landed');
|
||||
env.controller.destroy();
|
||||
stalled.resolve();
|
||||
await settle();
|
||||
assert.equal(imagery.destroyed, true);
|
||||
assert.equal(real.destroyed, true);
|
||||
assert.ok(!installed.includes(real));
|
||||
});
|
||||
Reference in new issue
Block a user