-
Notifications
You must be signed in to change notification settings - Fork 110
test: reproduce commonly cited timeout error #1390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a6d82ef
b09bab3
68c888b
e9c0add
57e5064
cbf59bf
eb61f46
19fbec5
4c230a1
30a7838
ae6f01c
e36bd3d
60303d6
0005a22
a9bf0df
941c6eb
2b4691d
1eacc70
8683a77
341e37e
e8b9ddc
ec849ca
6031197
1dbbc02
6a64eae
0f8fb72
d726c44
5bdd6bc
e7c0f43
c8312af
5b1bcaf
f144fd5
d5b3e63
6899406
ee1c0cd
7609fc0
676d561
9c2f9a7
015e5a6
69dbb30
a5f9730
5dbbefc
efa7631
e1e6599
8ffcea1
e36357d
a36a7de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,10 @@ | |
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // eslint-disable-next-line n/no-extraneous-import | ||
| import {ServiceError} from 'google-gax'; | ||
| import {Server} from '@grpc/grpc-js'; | ||
|
|
||
| const {dirname, resolve} = require('node:path'); | ||
|
|
||
| const PROTO_PATH = __dirname + '/../protos/google/datastore/v1/datastore.proto'; | ||
|
|
@@ -40,22 +44,43 @@ const descriptor = grpc.loadPackageDefinition(packageDefinition); | |
| /** | ||
| * Implements the runQuery RPC method. | ||
| */ | ||
| function grpcEndpoint( | ||
| call: {}, | ||
| callback: (arg1: string | null, arg2: {}) => {}, | ||
| function grpcEndpoint<ResponseType>( | ||
| call: CallType, | ||
| callback: MockServiceCallback<ResponseType>, | ||
| ) { | ||
| // SET A BREAKPOINT HERE AND EXPLORE `call` TO SEE THE REQUEST. | ||
| callback(null, {message: 'Hello'}); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This grpcEndpoint function has just been moved inside the only place it is used. |
||
| callback(null, {message: 'Hello'} as ResponseType); | ||
| } | ||
|
|
||
| export type CallType = () => {}; | ||
| export type GrpcErrorType = ServiceError | null; | ||
|
|
||
| type MockServiceCallback<ResponseType> = ( | ||
| arg1: GrpcErrorType, | ||
| arg2: ResponseType, | ||
| ) => {}; | ||
|
|
||
| interface MockServiceConfiguration<ResponseType> { | ||
| [endpoint: string]: ( | ||
| call: CallType, | ||
| callback: MockServiceCallback<ResponseType>, | ||
| ) => void; | ||
| } | ||
|
|
||
| /** | ||
| * Starts an RPC server that receives requests for datastore | ||
| */ | ||
| export function startServer(cb: () => void) { | ||
| export function startServer<ResponseType>( | ||
| cb: () => void, | ||
| serviceConfigurationOverride?: MockServiceConfiguration<ResponseType>, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is where a new service can be provided to the mock server. |
||
| ): Server { | ||
| const server = new grpc.Server(); | ||
| const service = descriptor.google.datastore.v1.Datastore.service; | ||
| // On the next line, change runQuery to the grpc method you want to investigate | ||
| server.addService(service, {runQuery: grpcEndpoint}); | ||
| const serviceConfiguration = serviceConfigurationOverride ?? { | ||
| runQuery: grpcEndpoint, | ||
| }; | ||
| server.addService(service, serviceConfiguration); | ||
| server.bindAsync( | ||
| '0.0.0.0:50051', | ||
| grpc.ServerCredentials.createInsecure(), | ||
|
|
@@ -64,4 +89,5 @@ export function startServer(cb: () => void) { | |
| cb(); | ||
| }, | ||
| ); | ||
| return server; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // Copyright 2025 Google LLC | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file contains utilities for working with the mock server that include a function for producing a deterministic set of errors and a function for shutting down the mock server. |
||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| import {CallType, GrpcErrorType} from '../mock-server/datastore-server'; | ||
|
|
||
| import grpc = require('@grpc/grpc-js'); | ||
| import {ServiceError} from 'google-gax'; | ||
|
|
||
| export class ErrorGenerator { | ||
| private errorSeriesCount = 0; | ||
|
|
||
| /** | ||
| * Generates an error object for testing purposes. | ||
| * | ||
| * This method creates a `ServiceError` object, simulating an error | ||
| * that might be returned by a gRPC service. The error includes a | ||
| * `DEADLINE_EXCEEDED` code (4), a details message indicating the number of | ||
| * errors generated so far by this instance, and some metadata. | ||
| * | ||
| * @returns {ServiceError} A `ServiceError` object representing a simulated | ||
| * gRPC error. | ||
| */ | ||
| generateError(code: number) { | ||
| // SET A BREAKPOINT HERE AND EXPLORE `call` TO SEE THE REQUEST. | ||
| this.errorSeriesCount++; | ||
| const metadata = new grpc.Metadata(); | ||
| metadata.set( | ||
| 'grpc-server-stats-bin', | ||
| Buffer.from([0, 0, 116, 73, 159, 3, 0, 0, 0, 0]), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is this buffer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
| ); | ||
| const error = new Error('error message') as ServiceError; | ||
| error.code = code; | ||
| error.details = `error details: error count: ${this.errorSeriesCount}`; | ||
| error.metadata = metadata; | ||
| return error; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a function that simulates an error response from a gRPC service. | ||
| * | ||
| * This method is designed to be used in mock server setups for testing purposes. | ||
| * It returns a function that, when called, will invoke a callback with a | ||
| * pre-configured error object, simulating a gRPC service responding with an error. | ||
| * The error includes a DEADLINE_EXCEEDED code (4), a details message indicating the | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why DEADLINE_EXCEEDED? Doesn't it take an arbitrary error code? |
||
| * number of errors generated so far by this instance, and some metadata. | ||
| * | ||
| * @param {number} code The grpc error code for the error sent back | ||
| * @returns {function} A function that takes a `call` object (representing the | ||
| * gRPC call) and a `callback` function, and responds to the call with a | ||
| * simulated error. | ||
| */ | ||
| sendErrorSeries<ResponseType>(code: number) { | ||
| return ( | ||
| call: CallType, | ||
| callback: (arg1: GrpcErrorType, arg2: ResponseType) => {}, | ||
| ) => { | ||
| const error = this.generateError(code); | ||
| callback(error, {} as ResponseType); | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| export function shutdownServer(server: grpc.Server) { | ||
| return new Promise((resolve, reject) => { | ||
| // Assuming 'server.tryShutdown' is a function that takes a callback. | ||
| // The callback is expected to be called when the shutdown attempt is complete. | ||
| // If 'tryShutdown' itself can throw an error or indicate an immediate failure | ||
| // (without calling the callback), you might need to wrap this call in a try...catch block. | ||
|
|
||
| server.tryShutdown((error: Error | undefined) => { | ||
| if (error) { | ||
| // If the callback is called with an error, reject the promise. | ||
| reject(error); | ||
| } else { | ||
| // If the callback is called without an error, resolve the promise. | ||
| resolve('done'); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Copyright 2025 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| import {describe, it} from 'mocha'; | ||
| import {Datastore} from '../src'; | ||
| import * as assert from 'assert'; | ||
|
|
||
| import {startServer} from '../mock-server/datastore-server'; | ||
| import {ErrorGenerator, shutdownServer} from './mock-server-tester'; | ||
| import {grpc} from 'google-gax'; | ||
|
|
||
| describe('Should make calls to runQuery', () => { | ||
| it('should report an UNAVAILABLE error to the user when it occurs with the original error details', done => { | ||
| const errorGenerator = new ErrorGenerator(); | ||
| const server = startServer( | ||
| async () => { | ||
| try { | ||
| try { | ||
| const datastore = new Datastore({ | ||
| namespace: `${Date.now()}`, | ||
| apiEndpoint: 'localhost:50051', | ||
| projectId: 'test-project', // Provided to avoid 'Unable to detect a Project Id in the current environment.' | ||
| }); | ||
| const postKey = datastore.key(['Post', 'post1']); | ||
| const query = datastore.createQuery('Post').hasAncestor(postKey); | ||
| // Make the call with a shorter timeout: | ||
| await datastore.runQuery(query, {gaxOptions: {timeout: 5000}}); | ||
| assert.fail('The call should not have succeeded'); | ||
| } catch (e) { | ||
| const message = (e as Error).message; | ||
| const searchValue = /[.*+?^${}()|[\]\\]/g; | ||
| const replaceValue = '\\$&'; | ||
| const substringToFind1 = | ||
| 'Total timeout of API google.datastore.v1.Datastore exceeded 5000 milliseconds retrying error Error: 14 UNAVAILABLE: error details: error count:'; | ||
| const escapedSubstringRegex1 = new RegExp( | ||
| substringToFind1.replace(searchValue, replaceValue), | ||
| ); | ||
| assert.match(message, escapedSubstringRegex1); | ||
| const substringToFind2 = | ||
| 'before any response was received. : Previous errors : [{message: 14 UNAVAILABLE: error details: error count: 1, code: 14, details: , note: },{message: 14 UNAVAILABLE: error details: error count: 2, code: 14, details: , note: },'; | ||
| const escapedSubstringRegex2 = new RegExp( | ||
| substringToFind2.replace(searchValue, replaceValue), | ||
| ); | ||
| assert.match(message, escapedSubstringRegex2); | ||
| done(); | ||
| } | ||
| } catch (e) { | ||
| done(e); | ||
| } | ||
| await shutdownServer(server); | ||
| }, | ||
| {runQuery: errorGenerator.sendErrorSeries(grpc.status.UNAVAILABLE)}, | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should probably be added to a package.json instead of disabling extraneous imports