mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Keep fallback retries single-flight and recover imagery and terrain independently
An activation that re-showed a fallback while a retry was in flight armed a second timer, so two retries could run at once and land out of order, leaving the cache and the screen on different providers. Retries now run one at a time; a request made meanwhile re-arms once the current one ends, and a result that is obsolete or no longer wanted is disposed. Imagery and terrain retries are now handled as each settles, so a stalled terrain attempt no longer keeps recovered imagery off screen, or the reverse.
This commit is contained in:
1 parent
bc499f1401
commit
f0d43a62e8
4 files changed
+138
-18
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
- A failed Esri Satellite or keyless terrain load still shows OSM or flat terrain
|
||||||
|
at once, but no longer lasts the whole session: while the fallback is on
|
||||||
|
screen, the real provider is retried in the background (30 seconds, doubling
|
||||||
|
to a 10-minute cap) and replaces it live once it loads. Map switches never
|
||||||
|
wait for a retry (Aditya, @Rohilalala, #1111).
|
||||||
@@ -386,8 +386,9 @@ upstream exposes it. One console warning is logged per cooldown window. Other
|
|||||||
failures keep Cesium's handling and the flat `EllipsoidTerrainProvider`
|
failures keep Cesium's handling and the flat `EllipsoidTerrainProvider`
|
||||||
fallback; while that flat terrain is on a shown globe the map controller
|
fallback; while that flat terrain is on a shown globe the map controller
|
||||||
retries Re:Earth in the background (30 s, doubling to a 10-minute cap) and
|
retries Re:Earth in the background (30 s, doubling to a 10-minute cap) and
|
||||||
installs the real terrain without waiting for a map switch. Behind a 3D tileset
|
installs the real terrain without waiting for a map switch or an imagery
|
||||||
it neither retries nor installs; the next globe activation does.
|
retry. Behind a 3D tileset it neither retries nor installs; the next globe
|
||||||
|
activation does.
|
||||||
`scripts/qa-terrain-429.mjs` reproduces the burst against a keyless
|
`scripts/qa-terrain-429.mjs` reproduces the burst against a keyless
|
||||||
dev server, counts real throttles with their headers as evidence, synthesises
|
dev server, counts real throttles with their headers as evidence, synthesises
|
||||||
throttles with `--inject N` when the upstream is not throttling, and fails if
|
throttles with `--inject N` when the upstream is not throttling, and fails if
|
||||||
|
|||||||
+48
-16
@@ -61,6 +61,8 @@ export class MapSourceController {
|
|||||||
this._terrainMode = null;
|
this._terrainMode = null;
|
||||||
this._terrainFallback = null;
|
this._terrainFallback = null;
|
||||||
this._retryTimer = null;
|
this._retryTimer = null;
|
||||||
|
this._retrying = false;
|
||||||
|
this._retryRequested = false;
|
||||||
this._retryDelay = FALLBACK_RETRY_MS;
|
this._retryDelay = FALLBACK_RETRY_MS;
|
||||||
this._subscribers = new Set();
|
this._subscribers = new Set();
|
||||||
this._destroyed = false;
|
this._destroyed = false;
|
||||||
@@ -329,9 +331,14 @@ export class MapSourceController {
|
|||||||
* screen: Esri's OSM stand-in, or flat terrain on a shown globe. Fallbacks
|
* screen: Esri's OSM stand-in, or flat terrain on a shown globe. Fallbacks
|
||||||
* stay cached and shown meanwhile, so no switch waits on a retry; a
|
* stay cached and shown meanwhile, so no switch waits on a retry; a
|
||||||
* fallback out of use is left alone until an activation shows it again.
|
* fallback out of use is left alone until an activation shows it again.
|
||||||
|
* One retry runs at a time; a request made meanwhile re-arms once it ends.
|
||||||
*/
|
*/
|
||||||
_scheduleRetry() {
|
_scheduleRetry() {
|
||||||
if (this._retryTimer) return;
|
if (this._retryTimer || this._destroyed) return;
|
||||||
|
if (this._retrying) {
|
||||||
|
this._retryRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._retryTimer = setTimeout(() => {
|
this._retryTimer = setTimeout(() => {
|
||||||
this._retryTimer = null;
|
this._retryTimer = null;
|
||||||
void this._retryFallbacks();
|
void this._retryFallbacks();
|
||||||
@@ -345,6 +352,8 @@ export class MapSourceController {
|
|||||||
let terrain = flat && this._sources.get(this._activeId)?.terrain;
|
let terrain = flat && this._sources.get(this._activeId)?.terrain;
|
||||||
if (terrain?.id !== this._terrainMode) terrain = null;
|
if (terrain?.id !== this._terrainMode) terrain = null;
|
||||||
if (!id && !terrain) return;
|
if (!id && !terrain) return;
|
||||||
|
this._retrying = true;
|
||||||
|
this._retryRequested = false;
|
||||||
const attempt = async (create) => {
|
const attempt = async (create) => {
|
||||||
try {
|
try {
|
||||||
return await create?.({ signal: this._abort.signal });
|
return await create?.({ signal: this._abort.signal });
|
||||||
@@ -352,18 +361,16 @@ export class MapSourceController {
|
|||||||
return null; // Still unavailable: keep the fallback.
|
return null; // Still unavailable: keep the fallback.
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const [provider, real] = await Promise.all([
|
// Each recovers as soon as it loads, never waiting on the other.
|
||||||
id && attempt(this._sources.get(id).imagery),
|
const retryImagery = async () => {
|
||||||
terrain && attempt(terrain.create),
|
const cached = this._imageryProviders.get(id);
|
||||||
]);
|
const provider = await attempt(this._sources.get(id).imagery);
|
||||||
if (this._destroyed) {
|
// Obsolete: destroyed, or the cache moved on while this was in flight.
|
||||||
|
if (this._destroyed || this._imageryProviders.get(id) !== cached) {
|
||||||
this._dispose(provider);
|
this._dispose(provider);
|
||||||
this._dispose(real?.provider);
|
return true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
let failed = (id && !provider) || (terrain && (!real || real.fallback));
|
if (!provider) return false;
|
||||||
try {
|
|
||||||
if (provider) {
|
|
||||||
this._imageryProviders.set(id, Promise.resolve(resolved(provider, id)));
|
this._imageryProviders.set(id, Promise.resolve(resolved(provider, id)));
|
||||||
// Still showing the fallback once any switch settles: use the real one.
|
// Still showing the fallback once any switch settles: use the real one.
|
||||||
const recover = () => {
|
const recover = () => {
|
||||||
@@ -377,22 +384,47 @@ export class MapSourceController {
|
|||||||
recover();
|
recover();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const retryTerrain = async () => {
|
||||||
|
const cached = this._terrainProviders.get(terrain.id);
|
||||||
|
const real = await attempt(terrain.create);
|
||||||
|
if (
|
||||||
|
this._destroyed ||
|
||||||
|
this._terrainProviders.get(terrain.id) !== cached ||
|
||||||
|
!real ||
|
||||||
|
real.fallback
|
||||||
|
) {
|
||||||
|
this._dispose(real?.provider);
|
||||||
|
return Boolean(real && !real.fallback);
|
||||||
}
|
}
|
||||||
if (terrain && real && !real.fallback) {
|
|
||||||
this._terrainProviders.set(terrain.id, Promise.resolve(real));
|
this._terrainProviders.set(terrain.id, Promise.resolve(real));
|
||||||
if (this._terrainFallback === flat && this.viewer.scene.globe.show) {
|
if (this._terrainFallback === flat && this.viewer.scene.globe.show) {
|
||||||
this._applyTerrain(real);
|
this._applyTerrain(real);
|
||||||
this._requestRender('map-stack');
|
this._requestRender('map-stack');
|
||||||
} else this._terrainMode = null; // The next globe activation installs it.
|
} else this._terrainMode = null; // The next globe activation installs it.
|
||||||
}
|
return true;
|
||||||
} catch (error) {
|
};
|
||||||
|
const settled = (retry) =>
|
||||||
|
retry().catch((error) => {
|
||||||
console.warn('[MapSourceController] fallback recovery failed:', error);
|
console.warn('[MapSourceController] fallback recovery failed:', error);
|
||||||
failed = true;
|
return false;
|
||||||
|
});
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
const results = await Promise.all([
|
||||||
|
id && settled(retryImagery),
|
||||||
|
terrain && settled(retryTerrain),
|
||||||
|
]);
|
||||||
|
failed = results.includes(false);
|
||||||
|
} finally {
|
||||||
|
this._retrying = false;
|
||||||
}
|
}
|
||||||
|
if (this._destroyed) return;
|
||||||
this._retryDelay = failed
|
this._retryDelay = failed
|
||||||
? Math.min(this._retryDelay * 2, FALLBACK_RETRY_MAX_MS)
|
? Math.min(this._retryDelay * 2, FALLBACK_RETRY_MAX_MS)
|
||||||
: FALLBACK_RETRY_MS;
|
: FALLBACK_RETRY_MS;
|
||||||
if (failed) this._scheduleRetry();
|
if (failed || this._retryRequested) this._scheduleRetry();
|
||||||
}
|
}
|
||||||
|
|
||||||
_cached(cache, id, create) {
|
_cached(cache, id, create) {
|
||||||
|
|||||||
@@ -1098,3 +1098,85 @@ test('destroy cancels a pending retry and releases one in flight', async (t) =>
|
|||||||
await settle();
|
await settle();
|
||||||
assert.equal(lateEsri.attempts, 2);
|
assert.equal(lateEsri.attempts, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('retries stay single-flight when an activation re-shows the fallback mid-retry', async (t) => {
|
||||||
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||||
|
const env = publicFixture();
|
||||||
|
const created = [],
|
||||||
|
pending = [];
|
||||||
|
let attempts = 0;
|
||||||
|
env.registry.sources.find(
|
||||||
|
(source) => source.descriptor.id === 'esri-imagery',
|
||||||
|
).imagery = async () => {
|
||||||
|
if (++attempts === 1) throw new Error('Esri unreachable');
|
||||||
|
const gate = deferred();
|
||||||
|
pending.push(gate);
|
||||||
|
await gate.promise;
|
||||||
|
const provider = { id: `esri-${attempts}`, errorEvent: event() };
|
||||||
|
provider.destroy = () => (provider.destroyed = true);
|
||||||
|
created.push(provider);
|
||||||
|
return provider;
|
||||||
|
};
|
||||||
|
await env.controller.setStack('esri-imagery');
|
||||||
|
t.mock.timers.tick(30_000);
|
||||||
|
await settle();
|
||||||
|
assert.equal(attempts, 2, 'retry #1 is in flight');
|
||||||
|
await env.controller.setStack('osm');
|
||||||
|
await env.controller.setStack('esri-imagery');
|
||||||
|
assert.equal(env.controller.getActiveId(), 'osm', 'the fallback is shown');
|
||||||
|
t.mock.timers.tick(30_000);
|
||||||
|
await settle();
|
||||||
|
assert.equal(attempts, 2, 'no second retry while one is in flight');
|
||||||
|
for (const gate of pending.reverse()) {
|
||||||
|
gate.resolve();
|
||||||
|
await settle();
|
||||||
|
}
|
||||||
|
const shown = env.imagery.at(-1).provider;
|
||||||
|
assert.equal(env.controller.getActiveId(), 'esri-imagery');
|
||||||
|
await env.controller.setStack('osm');
|
||||||
|
await env.controller.setStack('esri-imagery');
|
||||||
|
assert.equal(env.imagery.at(-1).provider, shown, 'the cached one is shown');
|
||||||
|
env.controller.destroy();
|
||||||
|
await settle();
|
||||||
|
assert.ok(created.length > 0);
|
||||||
|
for (const provider of created)
|
||||||
|
assert.equal(provider.destroyed, true, `${provider.id} is disposed`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stalled terrain retry does not hold back recovered imagery', async (t) => {
|
||||||
|
const stalled = deferred();
|
||||||
|
const { env, terrain } = terrainFixture(t, async (n) => {
|
||||||
|
if (n > 1) await stalled.promise;
|
||||||
|
return flatTerrain(n);
|
||||||
|
});
|
||||||
|
const esri = failingEsri(env);
|
||||||
|
await env.controller.setStack('esri-imagery');
|
||||||
|
assert.equal(env.controller.getActiveId(), 'osm');
|
||||||
|
esri.failing = false;
|
||||||
|
t.mock.timers.tick(30_000);
|
||||||
|
await settle();
|
||||||
|
assert.equal(terrain.calls, 2, 'the terrain retry is in flight');
|
||||||
|
assert.equal(env.controller.getActiveId(), 'esri-imagery');
|
||||||
|
assert.equal(env.imagery.at(-1).provider, env.providers.get('esri-imagery'));
|
||||||
|
stalled.resolve();
|
||||||
|
await settle();
|
||||||
|
env.controller.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stalled imagery retry does not hold back recovered terrain', async (t) => {
|
||||||
|
const real = { id: 'real' };
|
||||||
|
const { env, installed } = terrainFixture(t, async (n) =>
|
||||||
|
n > 1 ? { provider: real } : flatTerrain(n),
|
||||||
|
);
|
||||||
|
const esri = failingEsri(env);
|
||||||
|
await env.controller.setStack('esri-imagery');
|
||||||
|
const stalled = deferred();
|
||||||
|
esri.next = stalled.promise;
|
||||||
|
t.mock.timers.tick(30_000);
|
||||||
|
await settle();
|
||||||
|
assert.equal(esri.attempts, 2, 'the imagery retry is in flight');
|
||||||
|
assert.equal(installed.at(-1), real, 'real terrain is installed');
|
||||||
|
stalled.resolve();
|
||||||
|
await settle();
|
||||||
|
env.controller.destroy();
|
||||||
|
});
|
||||||
Reference in new issue
Block a user