mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 08:11:03 +00:00
fix(client): ask FOSSGIS when the routing relay times out, refuses the body or is missing
A route or height question through the relay now gives up after 12 s. A 400 without Valhalla's error_code and a 404 or 405 count as the relay unable to answer instead of no way or no Valhalla, and a Retry-After sets how long the tab leaves the relay alone.
This commit is contained in:
@@ -793,4 +793,56 @@ describe('through the TREK services (routing relay)', () => {
|
||||
expect(bodies[0]).not.toHaveProperty('shape')
|
||||
expect(bodies[0]).toMatchObject({ encoded_polyline: expect.any(String), costing: 'bicycle', shape_match: 'map_snap' })
|
||||
})
|
||||
|
||||
it('VALHALLA-RELAY-006: reads a 400 without error_code, and a route the server does not have, as the relay unable to answer', async () => {
|
||||
const refusals = [
|
||||
() => HttpResponse.json({ error: 'Expected number, received string' }, { status: 400 }),
|
||||
() => new HttpResponse('Not Found', { status: 404 }),
|
||||
() => HttpResponse.json({ error: 'Method Not Allowed' }, { status: 405 }),
|
||||
]
|
||||
for (const refusal of refusals) {
|
||||
resetValhallaTurns()
|
||||
let fossgis = 0
|
||||
server.use(
|
||||
http.post(RELAY, refusal),
|
||||
http.post(FOSSGIS_VALHALLA, () => { fossgis++; return HttpResponse.json(answer()) }),
|
||||
)
|
||||
await expect(valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'toll')).resolves.not.toBeNull()
|
||||
expect(fossgis).toBe(1)
|
||||
expect(valhallaBase()).toBe('https://valhalla1.openstreetmap.de')
|
||||
}
|
||||
})
|
||||
|
||||
it('VALHALLA-RELAY-007: rests the relay as long as its Retry-After says, at most an hour', async () => {
|
||||
for (const [retryAfter, restMs] of [['30', 30_000], ['86400', 60 * 60_000]] as const) {
|
||||
resetValhallaTurns()
|
||||
server.use(
|
||||
http.post(RELAY, () => HttpResponse.json({ error: 'spent' }, { status: 429, headers: { 'Retry-After': retryAfter } })),
|
||||
http.post(FOSSGIS_VALHALLA, () => HttpResponse.json(answer())),
|
||||
)
|
||||
await valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'toll')
|
||||
vi.useFakeTimers({ toFake: ['Date'] })
|
||||
vi.setSystemTime(Date.now() + restMs - 1000)
|
||||
expect(valhallaBase()).toBe('https://valhalla1.openstreetmap.de')
|
||||
vi.setSystemTime(Date.now() + 2000)
|
||||
expect(valhallaBase()).toBe(TREK_VALHALLA)
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('VALHALLA-RELAY-008: gives up on a relay that does not answer in time and asks FOSSGIS', async () => {
|
||||
const timeout = AbortSignal.timeout.bind(AbortSignal)
|
||||
const spy = vi.spyOn(AbortSignal, 'timeout').mockImplementation(ms =>
|
||||
ms === 12_000 ? AbortSignal.abort(new DOMException('signal timed out', 'TimeoutError')) : timeout(ms))
|
||||
let fossgis = 0
|
||||
server.use(
|
||||
http.post(RELAY, () => HttpResponse.json(answer())),
|
||||
http.post(FOSSGIS_VALHALLA, () => { fossgis++; return HttpResponse.json(answer()) }),
|
||||
)
|
||||
await expect(valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'toll')).resolves.not.toBeNull()
|
||||
expect(spy).toHaveBeenCalledWith(12_000)
|
||||
expect(fossgis).toBe(1)
|
||||
expect(valhallaBase()).toBe('https://valhalla1.openstreetmap.de')
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,6 +52,16 @@ const TREK_VALHALLA = '/api/routing/valhalla'
|
||||
*/
|
||||
const RELAY_REST_MS = 10 * 60_000
|
||||
|
||||
/** The longest Retry-After of the relay this tab follows; a longer one is asked about again after it. */
|
||||
const RELAY_MAX_WAIT_MS = 60 * 60_000
|
||||
|
||||
/**
|
||||
* How long a route or a height question may take through the relay. The server gives the
|
||||
* TREK Valhalla ten seconds; this leaves room for its 502 to arrive first, and where the
|
||||
* relay itself hangs, FOSSGIS answers soon after.
|
||||
*/
|
||||
const RELAY_TIMEOUT_MS = 12_000
|
||||
|
||||
/**
|
||||
* How far Valhalla may look for a road under a point, in metres.
|
||||
*
|
||||
@@ -388,22 +398,45 @@ export function resetValhallaTurns(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* The relay's statuses that say it cannot answer for the TREK services right now: no
|
||||
* session (a shared page), no key, a body it does not take, an allowance spent, a host
|
||||
* that is down. A 400 is Valhalla's own refusal of the way and is read as such.
|
||||
* Whether the relay's answer says it cannot answer for the TREK services right now: no
|
||||
* session (a shared page), no key, a body it does not take, a route this server does not
|
||||
* have yet, an allowance spent, a host that is down. A 400 that carries Valhalla's
|
||||
* `error_code` is its own refusal of the way and is read as such; one without is the
|
||||
* relay refusing the body.
|
||||
*/
|
||||
function relayCannotAnswer(status: number): boolean {
|
||||
return status === 401 || status === 403 || status === 413 || status === 429 || status >= 500
|
||||
async function relayCannotAnswer(response: Response): Promise<boolean> {
|
||||
if (response.status === 400) {
|
||||
const data: unknown = await response.clone().json().catch(() => null)
|
||||
return typeof (data as { error_code?: unknown } | null)?.error_code !== 'number'
|
||||
}
|
||||
return [401, 403, 404, 405, 413, 429].includes(response.status) || response.status >= 500
|
||||
}
|
||||
|
||||
/** The relay's Retry-After in milliseconds, as seconds or an HTTP date, or null without one. */
|
||||
function relayWait(response: Response): number | null {
|
||||
const value = response.headers.get('Retry-After')?.trim()
|
||||
if (!value) return null
|
||||
if (/^\d+$/.test(value)) return Number(value) * 1000
|
||||
const at = Date.parse(value)
|
||||
return Number.isNaN(at) ? null : Math.max(0, at - Date.now())
|
||||
}
|
||||
|
||||
async function askRelay(action: ValhallaRelayAction, body: object, signal?: AbortSignal): Promise<Response | null> {
|
||||
let bounded = signal
|
||||
// A description brings its own deadline; a route or a height gets the relay's.
|
||||
if (action !== 'trace_attributes') {
|
||||
const deadline = AbortSignal.timeout(RELAY_TIMEOUT_MS)
|
||||
bounded = signal ? AbortSignal.any([signal, deadline]) : deadline
|
||||
}
|
||||
let wait = RELAY_REST_MS
|
||||
try {
|
||||
const response = await valhallaRelayRepo.ask(action, body, signal)
|
||||
if (!relayCannotAnswer(response.status)) return response
|
||||
const response = await valhallaRelayRepo.ask(action, body, bounded)
|
||||
if (!(await relayCannotAnswer(response))) return response
|
||||
wait = Math.min(relayWait(response) ?? RELAY_REST_MS, RELAY_MAX_WAIT_MS)
|
||||
} catch (err) {
|
||||
if (signal?.aborted) throw err
|
||||
}
|
||||
relayRestingUntil = Date.now() + RELAY_REST_MS
|
||||
relayRestingUntil = Date.now() + wait
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user