fix(routes): keep the 12 s budget from arrival and name the outage to voice

Starting the upstream timeout after the outbound queue let a request run
up to about 19 s, past the Directions layer's 15 s; the budget again runs
from arrival, queue wait included, and a budget spent before the request
leaves the queue answers 429 "routing busy" rather than an outage.

plan_route threw "Routing HTTP 502" for an outage, and voice passes only
the message on; it now throws "Routing service unavailable (HTTP 502)".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
quanticsoul4772andClaude Opus 5.5 committed 2026-11-02 09:21:08 -08:00
1 parent 536f371dde
commit 1bff1961b4
6 files changed
+53 -25

No files matched your search

+8 -6
View File
@@ -311,15 +311,16 @@ test('callers sharing one failed upstream call all hear the outage, and it is no
assert.equal(calls, 2, 'the next request asks the routing service again');
});
test('a wait for an outbound slot does not count toward the upstream timeout', async (t) => {
test('a budget spent in the outbound queue is reported as busy, not as an outage', async (t) => {
_resetRouteUpstreamForTest();
// The second request waits 13 s for its slot, longer than the 12 s budget.
// The second request waits 13 s for its slot, past the 12 s budget.
_setRouteUpstreamIntervalForTest(13_000);
t.after(() => _resetRouteUpstreamForTest());
t.mock.timers.enable({ apis: ['setTimeout', 'Date'], now: Date.now() });
const request = install(installRouteMiddleware);
t.mock.method(globalThis, 'fetch', async (_url, { signal }) => {
signal.throwIfAborted();
let calls = 0;
t.mock.method(globalThis, 'fetch', async () => {
calls += 1;
return Response.json(osrmRouteWithSteps(2));
});
const first = request(
@@ -333,8 +334,9 @@ test('a wait for an outbound slot does not count toward the upstream timeout', a
assert.equal((await first).body.ok, true);
t.mock.timers.tick(13_000);
const result = await second;
assert.equal(result.statusCode, 200);
assert.equal(result.body.ok, true);
assert.equal(result.statusCode, 429);
assert.match(result.body.error, /routing busy/);
assert.equal(calls, 1, 'the spent request never reaches the routing service');
});
/** One OSRM route with `count` maneuvers, for the step tests. */
+7 -3
View File
@@ -190,9 +190,13 @@ export function createRouteService({
/* the status below is authoritative */
}
// A rate limit or an outage (5xx) is the routing service's failure, not
// a missing route, whatever the body says.
if (response.status === 429 || response.status >= 500)
throw httpFailure(response, 'Routing');
// a missing route, whatever the body says. Voice hears this message.
if (response.status === 429) throw httpFailure(response, 'Routing');
if (response.status >= 500)
throw new ToolError(
'unavailable',
`Routing service unavailable (HTTP ${response.status})`,
);
if (payload?.ok === false || response.status === 400)
return { ok: false, error: payload?.error || 'no route found' };
if (!response.ok) throw httpFailure(response, 'Routing');
+18
View File
@@ -283,6 +283,24 @@ test('a routing outage is reported as unavailable, not as invalid arguments', as
assert.equal(result.structuredContent.error, 'unavailable');
assert.doesNotMatch(result.content[0].text, /No walk route/);
}
// Voice passes only the message on, so it names the outage itself.
const outage = createRouteService({
fetchImpl: async () =>
Response.json(
{ ok: false, error: 'routing service unavailable' },
{ status: 502 },
),
});
await assert.rejects(
outage.route(
[
{ lat: 30, lon: -97 },
{ lat: 30.01, lon: -97.01 },
],
'foot',
),
{ message: 'Routing service unavailable (HTTP 502)' },
);
});
test('place answers say when the search limits may have left places out', async () => {