Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions src/route_building_blocks/routes_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,15 @@ import {APILoader} from '../api_loader/api_loader.js';
import {RequestErrorEvent} from '../base/events.js';
import {ComputeRoutesRequest, ComputeRoutesResponse, RouteConstructor} from '../utils/googlemaps_types.js';
import {RequestCache} from '../utils/request_cache.js';
import {isRetriableRpcError} from '../utils/rpc_status.js';

const CACHE_SIZE = 100;


function makeRoutesRequestCache() {
return new RequestCache<
ComputeRoutesRequest, ComputeRoutesResponse,
google.maps.MapsRequestError>(
CACHE_SIZE, (error: google.maps.MapsRequestError) => {
// The Routes API uses the RPCStatus enum for errors. Requests with a
// transient error status of RESOURCE_EXHAUSTED and UNKNOWN should be
// retried. See full list of statuses:
// https://developers.google.com/maps/documentation/javascript/reference/errors#RPCStatus
return error.code === 'RESOURCE_EXHAUSTED' ||
error.code === 'UNKNOWN';
});
google.maps.MapsRequestError>(CACHE_SIZE, isRetriableRpcError);
}

/**
Expand Down
12 changes: 10 additions & 2 deletions src/route_building_blocks/routes_controller_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,18 @@ describe('RoutesController', () => {
name: 'MapsRequestError',
message: 'The webpage is not allowed to use the Routes API.',
}
},
{
error: {
code: 'UNAVAILABLE',
endpoint: 'ROUTES_COMPUTE_ROUTES',
name: 'MapsRequestError',
message: 'The Routes API is temporarily unavailable.',
}
}
];

parameters.forEach(({error}) => {
for (const {error} of parameters) {
it(`retries failed request due to transient error: ${error.code}`,
async () => {
const host = await prepareControllerHostElement();
Expand All @@ -88,7 +96,7 @@ describe('RoutesController', () => {
await env.waitForStability();
expect(routesSpy).toHaveBeenCalledTimes(2);
});
});
}

it('does not retry failed request due to non transient error', async () => {
const host = await prepareControllerHostElement();
Expand Down
9 changes: 2 additions & 7 deletions src/store_locator/distances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import {APILoader} from '../api_loader/api_loader.js';
import {ComputeRouteMatrixRequest, ComputeRouteMatrixResponse, LatLng, LatLngLiteral, RouteMatrixConstructor} from '../utils/googlemaps_types.js';
import {RequestCache} from '../utils/request_cache.js';
import {isRetriableRpcError} from '../utils/rpc_status.js';

const CACHE_SIZE = 10;
// Self-imposed cap on how many destinations are sent to the Route Matrix API in
Expand All @@ -18,13 +19,7 @@ const MAX_ROUTE_MATRIX_DESTINATIONS = 25;
function makeRouteMatrixRequestCache() {
return new RequestCache<
ComputeRouteMatrixRequest, ComputeRouteMatrixResponse,
google.maps.MapsRequestError>(
CACHE_SIZE, (error: google.maps.MapsRequestError) => {
// Requests with a transient error status of RESOURCE_EXHAUSTED
// and UNKNOWN should be retried. See full list of statuses
// https://developers.google.com/maps/documentation/javascript/reference/errors#RPCStatus
return error.code === 'RESOURCE_EXHAUSTED' || error.code === 'UNKNOWN';
});
google.maps.MapsRequestError>(CACHE_SIZE, isRetriableRpcError);
}

/** How a distance was calculated. */
Expand Down
45 changes: 25 additions & 20 deletions src/store_locator/distances_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,31 @@ describe('DistanceMeasurer', () => {
expect(distances).toEqual([DEFAULT_FAKE_DISTANCE, DEFAULT_FAKE_DISTANCE]);
});

it('does not cache a transient error, RESOURCE_EXHAUSTED', async () => {
const routeMatrixSpy =
spyOn(env.fakeGoogleMapsHarness!, 'computeRouteMatrixHandler')
.and.throwError(
{code: 'RESOURCE_EXHAUSTED', name: 'MapsRequestError'} as
google.maps.MapsRequestError);
const origin = {lat: 0, lng: 0};
const destinations = [{lat: 1, lng: 1}, {lat: 2, lng: 2}];
const units = 0 as google.maps.UnitSystem.IMPERIAL;
const measurer = new DistanceMeasurer();

await expectAsync(measurer.computeDistances(origin, destinations, units))
.toBeRejected();
routeMatrixSpy.and.callThrough();
const distances =
await measurer.computeDistances(origin, destinations, units);

expect(routeMatrixSpy).toHaveBeenCalledTimes(2);
expect(distances).toEqual([DEFAULT_FAKE_DISTANCE, DEFAULT_FAKE_DISTANCE]);
});
const TRANSIENT_ERROR_CODES =
['RESOURCE_EXHAUSTED', 'UNAVAILABLE', 'UNKNOWN'];

for (const code of TRANSIENT_ERROR_CODES) {
it(`does not cache a transient error, ${code}`, async () => {
const routeMatrixSpy =
spyOn(env.fakeGoogleMapsHarness!, 'computeRouteMatrixHandler')
.and.throwError(
{code, name: 'MapsRequestError'} as
google.maps.MapsRequestError);
const origin = {lat: 0, lng: 0};
const destinations = [{lat: 1, lng: 1}, {lat: 2, lng: 2}];
const units = 0 as google.maps.UnitSystem.IMPERIAL;
const measurer = new DistanceMeasurer();

await expectAsync(measurer.computeDistances(origin, destinations, units))
.toBeRejected();
routeMatrixSpy.and.callThrough();
const distances =
await measurer.computeDistances(origin, destinations, units);

expect(routeMatrixSpy).toHaveBeenCalledTimes(2);
expect(distances).toEqual([DEFAULT_FAKE_DISTANCE, DEFAULT_FAKE_DISTANCE]);
});
}

it('caches a hard error, INVALID_ARGUMENT', async () => {
const routeMatrixSpy =
Expand Down
30 changes: 30 additions & 0 deletions src/utils/rpc_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* RPC statuses that indicate a transient failure.
* https://developers.google.com/maps/documentation/javascript/reference/errors#RPCStatus
*/
const RETRIABLE_RPC_STATUSES: readonly google.maps.RPCStatusString[] = [
'RESOURCE_EXHAUSTED',
'UNAVAILABLE',
'UNKNOWN',
];

/**
* Returns whether a failed Maps request should be retried, i.e. whether its
* failure should be kept out of the request cache so that an identical request
* can reach the service again.
*
* Returns true only for the transient `RPCStatus` codes above. A
* `MapsRequestError` raised by an API that reports a different status enum,
* such as `PlacesServiceStatus` or `GeocoderStatus`, is never retriable here.
*/
export function isRetriableRpcError(error: google.maps.MapsRequestError):
boolean {
// Widened to strings because `error.code` is not necessarily an RPCStatus.
return (RETRIABLE_RPC_STATUSES as readonly string[]).includes(error.code);
}
27 changes: 27 additions & 0 deletions src/utils/rpc_status_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

// import 'jasmine'; (google3-only)

import {isRetriableRpcError} from './rpc_status.js';

function makeError(code: string): google.maps.MapsRequestError {
return {code, name: 'MapsRequestError'} as google.maps.MapsRequestError;
}

describe('isRetriableRpcError', () => {
for (const code of ['RESOURCE_EXHAUSTED', 'UNAVAILABLE', 'UNKNOWN']) {
it(`returns true for transient status ${code}`, () => {
expect(isRetriableRpcError(makeError(code))).toBeTrue();
});
}

for (const code of ['INVALID_ARGUMENT', 'PERMISSION_DENIED', 'NOT_FOUND']) {
it(`returns false for non-transient status ${code}`, () => {
expect(isRetriableRpcError(makeError(code))).toBeFalse();
});
}
});
Loading