diff --git a/gax/src/clientInterface.ts b/gax/src/clientInterface.ts index 0d544e50c..94ae67701 100644 --- a/gax/src/clientInterface.ts +++ b/gax/src/clientInterface.ts @@ -37,6 +37,7 @@ export interface ClientOptions clientConfig?: gax.ClientConfig; fallback?: boolean | 'rest' | 'proto'; apiEndpoint?: string; + gaxServerStreamingRetries?: boolean; } export interface Descriptors { diff --git a/gax/src/createApiCall.ts b/gax/src/createApiCall.ts index 51a40bd3b..c51b613d1 100644 --- a/gax/src/createApiCall.ts +++ b/gax/src/createApiCall.ts @@ -28,10 +28,11 @@ import { SimpleCallbackFunction, } from './apitypes'; import {Descriptor} from './descriptor'; -import {CallOptions, CallSettings} from './gax'; +import {CallOptions, CallSettings, convertRetryOptions} from './gax'; import {retryable} from './normalCalls/retries'; import {addTimeoutArg} from './normalCalls/timeout'; import {StreamingApiCaller} from './streamingCalls/streamingApiCaller'; +import {warn} from './warnings'; /** * Converts an rpc call into an API call governed by the settings. @@ -63,7 +64,6 @@ export function createApiCall( // function. Currently client librares are only calling this method with a // promise, but it will change. const funcPromise = typeof func === 'function' ? Promise.resolve(func) : func; - // the following apiCaller will be used for all calls of this function... const apiCaller = createAPICaller(settings, descriptor); @@ -72,9 +72,22 @@ export function createApiCall( callOptions?: CallOptions, callback?: APICallback ) => { - const thisSettings = settings.merge(callOptions); - let currentApiCaller = apiCaller; + + let thisSettings: CallSettings; + if (currentApiCaller instanceof StreamingApiCaller) { + const gaxStreamingRetries = + currentApiCaller.descriptor?.gaxStreamingRetries ?? false; + // If Gax streaming retries are enabled, check settings passed at call time and convert parameters if needed + const convertedRetryOptions = convertRetryOptions( + callOptions, + gaxStreamingRetries + ); + thisSettings = settings.merge(convertedRetryOptions); + } else { + thisSettings = settings.merge(callOptions); + } + // special case: if bundling is disabled for this one call, // use default API caller instead if (settings.isBundling && !thisSettings.isBundling) { @@ -89,22 +102,42 @@ export function createApiCall( const streaming = (currentApiCaller as StreamingApiCaller).descriptor ?.streaming; + const retry = thisSettings.retry; + if ( - !streaming && + streaming && retry && - retry.retryCodes && - retry.retryCodes.length > 0 + retry.retryCodes.length > 0 && + retry.shouldRetryFn ) { - retry.backoffSettings.initialRpcTimeoutMillis = - retry.backoffSettings.initialRpcTimeoutMillis || - thisSettings.timeout; - return retryable( - func, - thisSettings.retry!, - thisSettings.otherArgs as GRPCCallOtherArgs, - thisSettings.apiName + warn( + 'either_retrycodes_or_shouldretryfn', + 'Only one of retryCodes or shouldRetryFn may be defined. Ignoring retryCodes.' ); + retry.retryCodes = []; + } + if (!streaming && retry) { + if (retry.shouldRetryFn) { + throw new Error( + 'Using a function to determine retry eligibility is only supported with server streaming calls' + ); + } + if (retry.getResumptionRequestFn) { + throw new Error( + 'Resumption strategy can only be used with server streaming retries' + ); + } + if (retry.retryCodes && retry.retryCodes.length > 0) { + retry.backoffSettings.initialRpcTimeoutMillis ??= + thisSettings.timeout; + return retryable( + func, + thisSettings.retry!, + thisSettings.otherArgs as GRPCCallOtherArgs, + thisSettings.apiName + ); + } } return addTimeoutArg( func, diff --git a/gax/src/gax.ts b/gax/src/gax.ts index 09e5d86ed..da17c1c2e 100644 --- a/gax/src/gax.ts +++ b/gax/src/gax.ts @@ -20,8 +20,11 @@ import type {Message} from 'protobufjs'; import {warn} from './warnings'; +import {GoogleError} from './googleError'; import {BundleOptions} from './bundlingCalls/bundleExecutor'; import {toLowerCamelCase} from './util'; +import {Status} from './status'; +import {RequestType} from './apitypes'; /** * Encapsulates the overridable settings for a particular API call. @@ -67,19 +70,46 @@ import {toLowerCamelCase} from './util'; /** * Per-call configurable settings for retrying upon transient failure. + * @implements {RetryOptionsType} * @typedef {Object} RetryOptions - * @property {String[]} retryCodes + * @property {number[]} retryCodes * @property {BackoffSettings} backoffSettings + * @property {(function)} shouldRetryFn + * @property {(function)} getResumptionRequestFn */ export class RetryOptions { retryCodes: number[]; backoffSettings: BackoffSettings; - constructor(retryCodes: number[], backoffSettings: BackoffSettings) { + shouldRetryFn?: (error: GoogleError) => boolean; + getResumptionRequestFn?: (request: RequestType) => RequestType; + constructor( + retryCodes: number[], + backoffSettings: BackoffSettings, + shouldRetryFn?: (error: GoogleError) => boolean, + getResumptionRequestFn?: (request: RequestType) => RequestType + ) { this.retryCodes = retryCodes; this.backoffSettings = backoffSettings; + this.shouldRetryFn = shouldRetryFn; + this.getResumptionRequestFn = getResumptionRequestFn; } } +/** + * Per-call configurable settings for working with retry-request + * See the repo README for more about the parameters + * https://github.com/googleapis/retry-request + * Will be deprecated in a future release. Only relevant to server streaming calls + * @typedef {Object} RetryOptions + * @property {boolean} objectMode - when true utilizes object mode in streams + * @property {request} request - the request to retry + * @property {number} noResponseRetries - number of times to retry on no response + * @property {number} currentRetryAttempt - what # retry attempt retry-request is on + * @property {Function} shouldRetryFn - determines whether to retry, returns a boolean + * @property {number} maxRetryDelay - maximum retry delay in seconds + * @property {number} retryDelayMultiplier - multiplier to increase the delay in between completion of failed requests + * @property {number} totalTimeout - total timeout in seconds + */ export interface RetryRequestOptions { objectMode?: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -87,7 +117,10 @@ export interface RetryRequestOptions { retries?: number; noResponseRetries?: number; currentRetryAttempt?: number; - shouldRetryFn?: () => boolean; + shouldRetryFn?: (error: GoogleError) => boolean; + maxRetryDelay?: number; + retryDelayMultiplier?: number; + totalTimeout?: number; } /** @@ -209,33 +242,21 @@ export class CallSettings { let longrunning = this.longrunning; let apiName = this.apiName; let retryRequestOptions = this.retryRequestOptions; - // If a method-specific timeout is set in the service config, and the retry codes for that - // method are non-null, then that timeout value will be used to - // override backoff settings. - if ( - retry !== undefined && - retry !== null && - retry.retryCodes !== null && - retry.retryCodes.length > 0 - ) { - retry.backoffSettings.initialRpcTimeoutMillis = timeout; - retry.backoffSettings.maxRpcTimeoutMillis = timeout; - retry.backoffSettings.totalTimeoutMillis = timeout; - } + // If the user provides a timeout to the method, that timeout value will be used // to override the backoff settings. if ('timeout' in options) { timeout = options.timeout!; - if ( - retry !== undefined && - retry !== null && - retry.retryCodes.length > 0 - ) { - retry.backoffSettings.initialRpcTimeoutMillis = timeout; - retry.backoffSettings.maxRpcTimeoutMillis = timeout; - retry.backoffSettings.totalTimeoutMillis = timeout; - } } + // If a method-specific timeout is set in the service config, and the retry codes for that + // method are non-null, then that timeout value will be used to + // override backoff settings. + if (retry?.retryCodes) { + retry!.backoffSettings.initialRpcTimeoutMillis = timeout; + retry!.backoffSettings.maxRpcTimeoutMillis = timeout; + retry!.backoffSettings.totalTimeoutMillis = timeout; + } + if ('retry' in options) { retry = mergeRetryOptions(retry || ({} as RetryOptions), options.retry!); } @@ -262,7 +283,7 @@ export class CallSettings { isBundling = options.isBundling!; } - if ('maxRetries' in options) { + if ('maxRetries' in options && options.maxRetries !== undefined) { retry!.backoffSettings!.maxRetries = options.maxRetries; delete retry!.backoffSettings!.totalTimeoutMillis; } @@ -293,22 +314,140 @@ export class CallSettings { } /** - * Per-call configurable settings for retrying upon transient failure. + * Validates passed retry options in preparation for eventual parameter deprecation + * converts retryRequestOptions to retryOptions + * then sets retryRequestOptions to null + * + * @param {CallOptions} options - a list of passed retry option + * @return {CallOptions} A new CallOptions object. * - * @param {number[]} retryCodes - a list of Google API canonical error codes + */ +export function convertRetryOptions( + options?: CallOptions, + gaxStreamingRetries?: boolean +): CallOptions | undefined { + // options will be undefined if no CallOptions object is passed at call time + if (!options) { + return options; + } + // if a user provided retry AND retryRequestOptions at call time, throw an error + // otherwise, convert supported parameters + if (!gaxStreamingRetries) { + if (options.retry) { + warn( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ); + } + if (options.retryRequestOptions) { + warn( + 'legacy_streaming_retry_request_behavior', + 'Legacy streaming retry behavior will not honor retryRequestOptions passed at call time. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will convert retryRequestOptions to retry parameters by default in future releases.', + 'DeprecationWarning' + ); + } + return options; + } + if (options.retry && options.retryRequestOptions) { + throw new Error('Only one of retry or retryRequestOptions may be set'); + } // handles parameter conversion from retryRequestOptions to retryOptions + if (options.retryRequestOptions) { + if (options.retryRequestOptions.objectMode !== undefined) { + warn( + 'retry_request_options', + 'objectMode override is not supported. It is set to true internally by default in gax.', + 'UnsupportedParameterWarning' + ); + } + if (options.retryRequestOptions.noResponseRetries !== undefined) { + warn( + 'retry_request_options', + 'noResponseRetries override is not supported. Please specify retry codes or a function to determine retry eligibility.', + 'UnsupportedParameterWarning' + ); + } + if (options.retryRequestOptions.currentRetryAttempt !== undefined) { + warn( + 'retry_request_options', + 'currentRetryAttempt override is not supported. Retry attempts are tracked internally.', + 'UnsupportedParameterWarning' + ); + } + + let retryCodes = [Status.UNAVAILABLE]; + let shouldRetryFn; + if (options.retryRequestOptions.shouldRetryFn) { + retryCodes = []; + shouldRetryFn = options.retryRequestOptions.shouldRetryFn; + } + + //Backoff settings + options.maxRetries = + options?.retryRequestOptions?.retries ?? options.maxRetries; + // create a default backoff settings object in case the user didn't provide overrides for everything + const backoffSettings = createDefaultBackoffSettings(); + let maxRetryDelayMillis; + let totalTimeoutMillis; + // maxRetryDelay - this is in seconds, need to convert to milliseconds + if (options.retryRequestOptions.maxRetryDelay !== undefined) { + maxRetryDelayMillis = options.retryRequestOptions.maxRetryDelay * 1000; + } + // retryDelayMultiplier - should be a one to one mapping to retryDelayMultiplier + const retryDelayMultiplier = + options?.retryRequestOptions?.retryDelayMultiplier ?? + backoffSettings.retryDelayMultiplier; + // this is in seconds and needs to be converted to milliseconds and the totalTimeoutMillis parameter + if (options.retryRequestOptions.totalTimeout !== undefined) { + totalTimeoutMillis = options.retryRequestOptions.totalTimeout * 1000; + } + + // for the variables the user wants to override, override in the backoff settings object we made + backoffSettings.maxRetryDelayMillis = + maxRetryDelayMillis ?? backoffSettings.maxRetryDelayMillis; + backoffSettings.retryDelayMultiplier = + retryDelayMultiplier ?? backoffSettings.retryDelayMultiplier; + backoffSettings.totalTimeoutMillis = + totalTimeoutMillis ?? backoffSettings.totalTimeoutMillis; + + const convertedRetryOptions = createRetryOptions( + retryCodes, + backoffSettings, + shouldRetryFn + ); + options.retry = convertedRetryOptions; + delete options.retryRequestOptions; // completely remove them to avoid any further confusion + warn( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ); + } + return options; +} + +/** + * Per-call configurable settings for retrying upon transient failure. + * @param {number[]} retryCodes - a list of Google API canonical error codes OR a function that returns a boolean to determine retry behavior * upon which a retry should be attempted. * @param {BackoffSettings} backoffSettings - configures the retry * exponential backoff algorithm. + * @param {function} shouldRetryFn - a function that determines whether a call should retry. If this is defined retryCodes must be empty + * @param {function} getResumptionRequestFn - a function with a resumption strategy - only used with server streaming retries * @return {RetryOptions} A new RetryOptions object. * */ export function createRetryOptions( retryCodes: number[], - backoffSettings: BackoffSettings + backoffSettings: BackoffSettings, + shouldRetryFn?: (error: GoogleError) => boolean, + getResumptionRequestFn?: (request: RequestType) => RequestType ): RetryOptions { return { retryCodes, backoffSettings, + shouldRetryFn, + getResumptionRequestFn, }; } @@ -479,7 +618,7 @@ function constructRetry( return null; } - let codes: number[] | null = null; + let codes: number[] | null = null; // this is one instance where it will NOT be an array OR a function because we do not allow shouldRetryFn in the client if (retryCodes && 'retry_codes_name' in methodConfig) { const retryCodesName = methodConfig['retry_codes_name']; codes = (retryCodes[retryCodesName!] || []).map(name => { @@ -526,16 +665,34 @@ function mergeRetryOptions( return null; } - if (!overrides.retryCodes && !overrides.backoffSettings) { + if ( + !overrides.retryCodes && + !overrides.backoffSettings && + !overrides.shouldRetryFn && + !overrides.getResumptionRequestFn + ) { return retry; } - const codes = overrides.retryCodes ? overrides.retryCodes : retry.retryCodes; + const retryCodes = overrides.retryCodes + ? overrides.retryCodes + : retry.retryCodes; const backoffSettings = overrides.backoffSettings ? overrides.backoffSettings : retry.backoffSettings; - return createRetryOptions(codes!, backoffSettings!); + const shouldRetryFn = overrides.shouldRetryFn + ? overrides.shouldRetryFn + : retry.shouldRetryFn; + const getResumptionRequestFn = overrides.getResumptionRequestFn + ? overrides.getResumptionRequestFn + : retry.getResumptionRequestFn; + return createRetryOptions( + retryCodes!, + backoffSettings!, + shouldRetryFn!, + getResumptionRequestFn! + ); } export interface ServiceConfig { diff --git a/gax/src/normalCalls/retries.ts b/gax/src/normalCalls/retries.ts index 2a6e6ab52..2377b1360 100644 --- a/gax/src/normalCalls/retries.ts +++ b/gax/src/normalCalls/retries.ts @@ -107,7 +107,10 @@ export function retryable( return; } canceller = null; - if (retry.retryCodes.indexOf(err!.code!) < 0) { + if ( + retry.retryCodes.length > 0 && + retry.retryCodes.indexOf(err!.code!) < 0 + ) { err.note = 'Exception occurred in retry method that was ' + 'not classified as transient'; diff --git a/gax/src/streamingCalls/streamDescriptor.ts b/gax/src/streamingCalls/streamDescriptor.ts index d23f80219..fc6090d09 100644 --- a/gax/src/streamingCalls/streamDescriptor.ts +++ b/gax/src/streamingCalls/streamDescriptor.ts @@ -16,8 +16,6 @@ import {APICaller} from '../apiCaller'; import {Descriptor} from '../descriptor'; -import {CallSettings} from '../gax'; - import {StreamType} from './streaming'; import {StreamingApiCaller} from './streamingApiCaller'; @@ -28,19 +26,23 @@ export class StreamDescriptor implements Descriptor { type: StreamType; streaming: boolean; // needed for browser support rest?: boolean; + gaxStreamingRetries?: boolean; - constructor(streamType: StreamType, rest?: boolean) { + constructor( + streamType: StreamType, + rest?: boolean, + gaxStreamingRetries?: boolean + ) { this.type = streamType; this.streaming = true; this.rest = rest; + this.gaxStreamingRetries = gaxStreamingRetries; } - getApiCaller(settings: CallSettings): APICaller { + getApiCaller(): APICaller { // Right now retrying does not work with gRPC-streaming, because retryable // assumes an API call returns an event emitter while gRPC-streaming methods // return Stream. - // TODO: support retrying. - settings.retry = null; return new StreamingApiCaller(this); } } diff --git a/gax/src/streamingCalls/streaming.ts b/gax/src/streamingCalls/streaming.ts index ec3192160..c55b5ffe2 100644 --- a/gax/src/streamingCalls/streaming.ts +++ b/gax/src/streamingCalls/streaming.ts @@ -22,10 +22,13 @@ import { APICallback, CancellableStream, GRPCCallResult, + RequestType, SimpleCallbackFunction, } from '../apitypes'; -import {RetryRequestOptions} from '../gax'; +import {RetryOptions, RetryRequestOptions} from '../gax'; import {GoogleError} from '../googleError'; +import {streamingRetryRequest} from '../streamingRetryRequest'; +import {Status} from '../status'; // eslint-disable-next-line @typescript-eslint/no-var-requires const duplexify: DuplexifyConstructor = require('duplexify'); @@ -84,6 +87,11 @@ export class StreamProxy extends duplexify implements GRPCCallResult { stream?: CancellableStream; private _responseHasSent: boolean; rest?: boolean; + gaxServerStreamingRetries?: boolean; + apiCall?: SimpleCallbackFunction; + argument?: {}; + prevDeadline?: number; + retries?: number = 0; /** * StreamProxy is a proxy to gRPC-streaming method. * @@ -92,7 +100,12 @@ export class StreamProxy extends duplexify implements GRPCCallResult { * @param {StreamType} type - the type of gRPC stream. * @param {ApiCallback} callback - the callback for further API call. */ - constructor(type: StreamType, callback: APICallback, rest?: boolean) { + constructor( + type: StreamType, + callback: APICallback, + rest?: boolean, + gaxServerStreamingRetries?: boolean + ) { super(undefined, undefined, { objectMode: true, readable: type !== StreamType.CLIENT_STREAMING, @@ -103,6 +116,7 @@ export class StreamProxy extends duplexify implements GRPCCallResult { this._isCancelCalled = false; this._responseHasSent = false; this.rest = rest; + this.gaxServerStreamingRetries = gaxServerStreamingRetries; } cancel() { @@ -113,10 +127,178 @@ export class StreamProxy extends duplexify implements GRPCCallResult { } } + retry(stream: CancellableStream, retry: RetryOptions) { + let retryArgument = this.argument! as unknown as RequestType; + if (typeof retry.getResumptionRequestFn! === 'function') { + const resumptionRetryArgument = + retry.getResumptionRequestFn(retryArgument); + if (resumptionRetryArgument !== undefined) { + retryArgument = retry.getResumptionRequestFn(retryArgument); + } + } + this.resetStreams(stream); + + const newStream = this.apiCall!( + retryArgument, + this._callback + ) as CancellableStream; + this.stream = newStream; + + this.streamHandoffHelper(newStream, retry); + return newStream; + } + + /** + * Helper function to handle total timeout + max retry check for server streaming retries + * @param {number} deadline - the current retry deadline + * @param {number} maxRetries - maximum total number of retries + * @param {number} totalTimeoutMillis - total timeout in milliseconds + */ + throwIfMaxRetriesOrTotalTimeoutExceeded( + deadline: number, + maxRetries: number, + totalTimeoutMillis: number + ): void { + const now = new Date(); + + if ( + this.prevDeadline! !== undefined && + deadline && + now.getTime() >= this.prevDeadline + ) { + const error = new GoogleError( + `Total timeout of API exceeded ${totalTimeoutMillis} milliseconds before any response was received.` + ); + error.code = Status.DEADLINE_EXCEEDED; + this.emit('error', error); + this.destroy(); + // Without throwing error you get unhandled error since we are returning a new stream + // There might be a better way to do this + throw error; + } + + if (this.retries && this.retries >= maxRetries) { + const error = new GoogleError( + 'Exceeded maximum number of retries before any ' + + 'response was received' + ); + error.code = Status.DEADLINE_EXCEEDED; + this.emit('error', error); + this.destroy(); + throw error; + } + } + + /** + * Error handler for server streaming retries + * @param {CancellableStream} stream - the stream being retried + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function should retry, and the parameters to the exponential backoff retry + * algorithm. + * @param {Error} error - error to handle + */ + streamHandoffErrorHandler( + stream: CancellableStream, + retry: RetryOptions, + error: Error + ): void { + let retryStream = this.stream; + const delayMult = retry.backoffSettings.retryDelayMultiplier; + const maxDelay = retry.backoffSettings.maxRetryDelayMillis; + const timeoutMult = retry.backoffSettings.rpcTimeoutMultiplier; + const maxTimeout = retry.backoffSettings.maxRpcTimeoutMillis; + + let delay = retry.backoffSettings.initialRetryDelayMillis; + let timeout = retry.backoffSettings.initialRpcTimeoutMillis; + let now = new Date(); + let deadline = 0; + + if (retry.backoffSettings.totalTimeoutMillis) { + deadline = now.getTime() + retry.backoffSettings.totalTimeoutMillis; + } + const maxRetries = retry.backoffSettings.maxRetries!; + try { + this.throwIfMaxRetriesOrTotalTimeoutExceeded( + deadline, + maxRetries, + retry.backoffSettings.totalTimeoutMillis! + ); + } catch (error) { + return; + } + + this.retries!++; + const e = GoogleError.parseGRPCStatusDetails(error); + let shouldRetry = this.defaultShouldRetry(e!, retry); + if (retry.shouldRetryFn) { + shouldRetry = retry.shouldRetryFn(e!); + } + + if (shouldRetry) { + const toSleep = Math.random() * delay; + setTimeout(() => { + now = new Date(); + delay = Math.min(delay * delayMult, maxDelay); + const timeoutCal = timeout && timeoutMult ? timeout * timeoutMult : 0; + const rpcTimeout = maxTimeout ? maxTimeout : 0; + this.prevDeadline = deadline; + const newDeadline = deadline ? deadline - now.getTime() : 0; + timeout = Math.min(timeoutCal, rpcTimeout, newDeadline); + }, toSleep); + } else { + e.note = + 'Exception occurred in retry method that was ' + + 'not classified as transient'; + // for some reason this error must be emitted here + // instead of the destroy, otherwise the error event + // is swallowed + this.emit('error', e); + this.destroy(); + return; + } + retryStream = this.retry(stream, retry); + this.stream = retryStream; + return; + } + + /** + * Used during server streaming retries to handle + * event forwarding, errors, and/or stream closure + * @param {CancellableStream} stream - the stream that we're doing the retry on + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function should retry, and the parameters to the exponential backoff retry + * algorithm. + */ + streamHandoffHelper(stream: CancellableStream, retry: RetryOptions): void { + let enteredError = false; + const eventsToForward = ['metadata', 'response', 'status', 'data']; + + eventsToForward.forEach(event => { + stream.on(event, this.emit.bind(this, event)); + }); + + stream.on('error', error => { + enteredError = true; + this.streamHandoffErrorHandler(stream, retry, error); + }); + + stream.on('end', () => { + if (!enteredError) { + enteredError = true; + this.emit('end'); + this.cancel(); + } + }); + } + /** * Forward events from an API request stream to the user's stream. * @param {Stream} stream - The API request stream. + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function should retry, and the parameters to the exponential backoff retry + * algorithm. */ + forwardEvents(stream: Stream) { const eventsToForward = ['metadata', 'response', 'status']; eventsToForward.forEach(event => { @@ -158,21 +340,162 @@ export class StreamProxy extends duplexify implements GRPCCallResult { }); } + defaultShouldRetry(error: GoogleError, retry: RetryOptions) { + if ( + retry.retryCodes.length > 0 && + retry.retryCodes.indexOf(error!.code!) < 0 + ) { + return false; + } + return true; + } + + /** + * Forward events from an API request stream to the user's stream. + * @param {Stream} stream - The API request stream. + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function eshould retry, and the parameters to the exponential backoff retry + * algorithm. + */ + forwardEventsWithRetries( + stream: CancellableStream, + retry: RetryOptions + ): CancellableStream | undefined { + let retryStream = this.stream; + const eventsToForward = ['metadata', 'response', 'status']; + eventsToForward.forEach(event => { + stream.on(event, this.emit.bind(this, event)); + }); + // gRPC is guaranteed emit the 'status' event but not 'metadata', and 'status' is the last event to emit. + // Emit the 'response' event if stream has no 'metadata' event. + // This avoids the stream swallowing the other events, such as 'end'. + stream.on('status', () => { + if (!this._responseHasSent) { + stream.emit('response', { + code: 200, + details: '', + message: 'OK', + }); + } + }); + + // We also want to supply the status data as 'response' event to support + // the behavior of google-cloud-node expects. + // see: + // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1775#issuecomment-259141029 + // https://github.com/GoogleCloudPlatform/google-cloud-node/blob/116436fa789d8b0f7fc5100b19b424e3ec63e6bf/packages/common/src/grpc-service.js#L355 + stream.on('metadata', metadata => { + // Create a response object with succeeds. + // TODO: unify this logic with the decoration of gRPC response when it's + // added. see: https://github.com/googleapis/gax-nodejs/issues/65 + stream.emit('response', { + code: 200, + details: '', + message: 'OK', + metadata, + }); + this._responseHasSent = true; + }); + + stream.on('error', error => { + const timeout = retry.backoffSettings.totalTimeoutMillis; + const maxRetries = retry.backoffSettings.maxRetries!; + if ((maxRetries && maxRetries > 0) || (timeout && timeout > 0)) { + const e = GoogleError.parseGRPCStatusDetails(error); + let shouldRetry = this.defaultShouldRetry(e!, retry); + if (retry.shouldRetryFn) { + shouldRetry = retry.shouldRetryFn(e!); + } + + if (shouldRetry) { + if (maxRetries && timeout!) { + const newError = new GoogleError( + 'Cannot set both totalTimeoutMillis and maxRetries ' + + 'in backoffSettings.' + ); + newError.code = Status.INVALID_ARGUMENT; + this.emit('error', newError); + this.destroy(); + return; //end chunk + } else { + retryStream = this.retry(stream, retry); + this.stream = retryStream; + return retryStream; + } + } else { + const e = GoogleError.parseGRPCStatusDetails(error); + e.note = + 'Exception occurred in retry method that was ' + + 'not classified as transient'; + this.destroy(e); + return; // end chunk + } + } else { + return GoogleError.parseGRPCStatusDetails(error); + } + }); + return retryStream; + } + + /** + * Resets the target stream as part of the retry process + * @param {CancellableStream} requestStream - the stream to end + */ + resetStreams(requestStream: CancellableStream) { + if (requestStream) { + requestStream.cancel && requestStream.cancel(); + if (requestStream.destroy) { + requestStream.destroy(); + } else if (requestStream.end) { + // TODO: not used in server streaming, but likely needed + // if we want to add BIDI or client side streaming + requestStream.end(); + } + } + } + /** * Specifies the target stream. * @param {ApiCall} apiCall - the API function to be called. * @param {Object} argument - the argument to be passed to the apiCall. + * @param {RetryOptions} retry - Configures the exceptions upon which the + * function should retry, and the parameters to the exponential backoff retry + * algorithm. */ setStream( apiCall: SimpleCallbackFunction, argument: {}, - retryRequestOptions: RetryRequestOptions = {} + retryRequestOptions: RetryRequestOptions = {}, + retry: RetryOptions ) { + this.apiCall = apiCall; + this.argument = argument; + if (this.type === StreamType.SERVER_STREAMING) { if (this.rest) { const stream = apiCall(argument, this._callback) as CancellableStream; this.stream = stream; this.setReadable(stream); + } else if (this.gaxServerStreamingRetries) { + const retryStream = streamingRetryRequest({ + request: () => { + if (this._isCancelCalled) { + if (this.stream) { + this.stream.cancel(); + } + return; + } + const stream = apiCall( + argument, + this._callback + ) as CancellableStream; + this.stream = stream; + this.stream = this.forwardEventsWithRetries(stream, retry); + return this.stream; + }, + }); + + this.setReadable(retryStream); } else { const retryStream = retryRequest(null, { objectMode: true, diff --git a/gax/src/streamingCalls/streamingApiCaller.ts b/gax/src/streamingCalls/streamingApiCaller.ts index 6f8dc3634..68ddb9855 100644 --- a/gax/src/streamingCalls/streamingApiCaller.ts +++ b/gax/src/streamingCalls/streamingApiCaller.ts @@ -47,7 +47,8 @@ export class StreamingApiCaller implements APICaller { return new StreamProxy( this.descriptor.type, callback, - this.descriptor.rest + this.descriptor.rest, + this.descriptor.gaxStreamingRetries ); } @@ -85,7 +86,12 @@ export class StreamingApiCaller implements APICaller { settings: CallSettings, stream: StreamProxy ) { - stream.setStream(apiCall, argument, settings.retryRequestOptions); + stream.setStream( + apiCall, + argument, + settings.retryRequestOptions, + settings.retry! + ); } fail(stream: CancellableStream, err: Error) { diff --git a/gax/src/streamingRetryRequest.ts b/gax/src/streamingRetryRequest.ts new file mode 100644 index 000000000..e97f64355 --- /dev/null +++ b/gax/src/streamingRetryRequest.ts @@ -0,0 +1,113 @@ +// Copyright 2023 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 + +// https://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. + +const {PassThrough} = require('stream'); +import {GoogleError} from './googleError'; +import {ResponseType} from './apitypes'; +import {StreamProxy} from './streamingCalls/streaming'; + +const DEFAULTS = { + /* + Max # of retries + */ + maxRetries: 2, +}; +// In retry-request, you could pass parameters to request using the requestOpts parameter +// when we called retry-request from gax, we always passed null +// passing null here removes an unnecessary parameter from this implementation +const requestOps = null; +const objectMode = true; // we don't support objectMode being false + +interface streamingRetryRequestOptions { + request?: Function; + maxRetries?: number; +} +/** + * Localized adaptation derived from retry-request + * @param opts - corresponds to https://github.com/googleapis/retry-request#opts-optional + * @returns + */ +export function streamingRetryRequest(opts: streamingRetryRequestOptions) { + opts = Object.assign({}, DEFAULTS, opts); + + if (opts.request === undefined) { + try { + // eslint-disable-next-line node/no-unpublished-require + opts.request = require('request'); + } catch (e) { + throw new Error('A request library must be provided to retry-request.'); + } + } + + let numNoResponseAttempts = 0; + let streamResponseHandled = false; + + let requestStream: StreamProxy; + let delayStream: StreamProxy; + + const retryStream = new PassThrough({objectMode: objectMode}); + + makeRequest(); + return retryStream; + + function makeRequest() { + streamResponseHandled = false; + + delayStream = new PassThrough({objectMode: objectMode}); + requestStream = opts.request!(requestOps); + + requestStream + // gRPC via google-cloud-node can emit an `error` as well as a `response` + // Whichever it emits, we run with-- we can't run with both. That's what + // is up with the `streamResponseHandled` tracking. + .on('error', (err: GoogleError) => { + if (streamResponseHandled) { + return; + } + streamResponseHandled = true; + onResponse(err); + }) + .on('response', (resp: ResponseType) => { + if (streamResponseHandled) { + return; + } + + streamResponseHandled = true; + onResponse(null, resp); + }); + requestStream.pipe(delayStream); + } + + function onResponse(err: GoogleError | null, response: ResponseType = null) { + // An error such as DNS resolution. + if (err) { + numNoResponseAttempts++; + + if (numNoResponseAttempts <= opts.maxRetries!) { + makeRequest(); + } else { + retryStream.emit('error', err); + } + + return; + } + + // No more attempts need to be made, just continue on. + retryStream.emit('response', response); + delayStream.pipe(retryStream); + requestStream.on('error', (err: GoogleError) => { + retryStream.destroy(err); + }); + } +} diff --git a/gax/test/browser-test/test/test.grpc-fallback.ts b/gax/test/browser-test/test/test.grpc-fallback.ts index 6cf85e725..030cbcac6 100644 --- a/gax/test/browser-test/test/test.grpc-fallback.ts +++ b/gax/test/browser-test/test/test.grpc-fallback.ts @@ -93,8 +93,8 @@ describe('createStub', () => { assert(echoStub.pagedExpand instanceof Function); assert(echoStub.wait instanceof Function); - // There should be 8 methods for the echo service - assert.strictEqual(Object.keys(echoStub).length, 8); + // There should be 10 methods for the echo service + assert.strictEqual(Object.keys(echoStub).length, 10); // Each of the service methods should take 4 arguments (so that it works // with createApiCall) @@ -109,8 +109,8 @@ describe('createStub', () => { assert(echoStub.collect instanceof Function); assert(echoStub.chat instanceof Function); - // There should be 8 methods for the echo service - assert.strictEqual(Object.keys(echoStub).length, 8); + // There should be 10 methods for the echo service + assert.strictEqual(Object.keys(echoStub).length, 10); // Each of the service methods should take 4 arguments (so that it works // with createApiCall) @@ -229,10 +229,7 @@ describe('grpc-fallback', () => { const options: any = {}; options.otherArgs = {}; options.otherArgs.headers = {}; - options.otherArgs.headers['x-goog-request-params'] = - fallback.routingHeader.fromParams({ - abc: 'def', - }); + options.otherArgs.headers['x-test-header'] = 'value'; const response = requestObject; // eslint-disable-next-line no-undef const savedFetch = window.fetch; @@ -240,7 +237,7 @@ describe('grpc-fallback', () => { // eslint-disable-next-line no-undef window.fetch = (url, options) => { // @ts-ignore - assert.strictEqual(options.headers['x-goog-request-params'], 'abc=def'); + assert.strictEqual(options.headers['x-test-header'], 'value'); return Promise.resolve({ ok: true, arrayBuffer: () => { diff --git a/gax/test/showcase-echo-client/package.json b/gax/test/showcase-echo-client/package.json index d2351a533..1335ac937 100644 --- a/gax/test/showcase-echo-client/package.json +++ b/gax/test/showcase-echo-client/package.json @@ -10,7 +10,6 @@ "build/src", "build/protos" ], - "types": "build/src/index.d.ts", "keywords": [ "google apis client", "google api client", @@ -23,12 +22,11 @@ "google showcase", "showcase", "echo", - "identity", - "messaging", - "testing" + "sequence service" ], "scripts": { - "compile": "tsc -p . && cp -r protos build/", + "clean": "gts clean", + "compile": "tsc -p . && cp -r protos build/ && minifyProtoJson", "compile-protos": "compileProtos src", "prefetch": "rm -rf node_modules package-lock.json google-gax*.tgz gapic-tools*.tgz && cd ../.. && npm pack && mv google-gax*.tgz test/showcase-echo-client/google-gax.tgz && cd ../tools && npm install && npm pack && mv gapic-tools*.tgz ../gax/test/showcase-echo-client/gapic-tools.tgz", "prepare": "npm run compile-protos && npm run compile" diff --git a/gax/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto b/gax/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto index 6e9d78a37..3f79b4457 100644 --- a/gax/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto +++ b/gax/test/showcase-echo-client/protos/google/showcase/v1beta1/echo.proto @@ -17,6 +17,7 @@ syntax = "proto3"; import "google/api/annotations.proto"; import "google/api/client.proto"; import "google/api/field_behavior.proto"; +import "google/api/routing.proto"; import "google/longrunning/operations.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; @@ -27,27 +28,63 @@ package google.showcase.v1beta1; option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; option java_package = "com.google.showcase.v1beta1"; option java_multiple_files = true; +option ruby_package = "Google::Showcase::V1beta1"; // This service is used showcase the four main types of rpcs - unary, server // side streaming, client side streaming, and bidirectional streaming. This // service also exposes methods that explicitly implement server delay, and // paginated calls. Set the 'showcase-trailer' metadata key on any method -// to have the values echoed in the response trailers. +// to have the values echoed in the response trailers. Set the +// 'x-goog-request-params' metadata key on any method to have the values +// echoed in the response headers. service Echo { // This service is meant to only run locally on the port 7469 (keypad digits // for "show"). option (google.api.default_host) = "localhost:7469"; - // This method simply echos the request. This method is showcases unary rpcs. + // This method simply echoes the request. This method showcases unary RPCs. rpc Echo(EchoRequest) returns (EchoResponse) { option (google.api.http) = { post: "/v1beta1/echo:echo" body: "*" }; + option (google.api.routing) = { + routing_parameters{ + field: "header" + } + routing_parameters{ + field: "header" + path_template: "{routing_id=**}" + } + routing_parameters{ + field: "header" + path_template: "{table_name=regions/*/zones/*/**}" + } + routing_parameters{ + field: "header" + path_template: "{super_id=projects/*}/**" + } + routing_parameters{ + field: "header" + path_template: "{table_name=projects/*/instances/*/**}" + } + routing_parameters{ + field: "header" + path_template: "projects/*/{instance_id=instances/*}/**" + } + routing_parameters{ + field: "other_header" + path_template: "{baz=**}" + } + routing_parameters{ + field: "other_header" + path_template: "{qux=projects/*}/**" + } + }; } - // This method split the given content into words and will pass each word back - // through the stream. This method showcases server-side streaming rpcs. + // This method splits the given content into words and will pass each word back + // through the stream. This method showcases server-side streaming RPCs. rpc Expand(ExpandRequest) returns (stream EchoResponse) { option (google.api.http) = { post: "/v1beta1/echo:expand" @@ -60,7 +97,7 @@ service Echo { // This method will collect the words given to it. When the stream is closed // by the client, this method will return the a concatenation of the strings - // passed to it. This method showcases client-side streaming rpcs. + // passed to it. This method showcases client-side streaming RPCs. rpc Collect(stream EchoRequest) returns (EchoResponse) { option (google.api.http) = { post: "/v1beta1/echo:collect" @@ -68,9 +105,9 @@ service Echo { }; } - // This method, upon receiving a request on the stream, the same content will - // be passed back on the stream. This method showcases bidirectional - // streaming rpcs. + // This method, upon receiving a request on the stream, will pass the same + // content back on the stream. This method showcases bidirectional + // streaming RPCs. rpc Chat(stream EchoRequest) returns (stream EchoResponse); // This is similar to the Expand method but instead of returning a stream of @@ -82,8 +119,30 @@ service Echo { }; } - // This method will wait the requested amount of and then return. - // This method showcases how a client handles a request timing out. + // This is similar to the PagedExpand except that it uses + // max_results instead of page_size, as some legacy APIs still + // do. New APIs should NOT use this pattern. + rpc PagedExpandLegacy(PagedExpandLegacyRequest) returns (PagedExpandResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:pagedExpandLegacy" + body: "*" + }; + } + + // This method returns a map containing lists of words that appear in the input, keyed by their + // initial character. The only words returned are the ones included in the current page, + // as determined by page_token and page_size, which both refer to the word indices in the + // input. This paging result consisting of a map of lists is a pattern used by some legacy + // APIs. New APIs should NOT use this pattern. + rpc PagedExpandLegacyMapped(PagedExpandRequest) returns (PagedExpandLegacyMappedResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:pagedExpandLegacyMapped" + body: "*" + }; + } + + // This method will wait for the requested amount of time and then return. + // This method showcases how a client handles a request timeout. rpc Wait(WaitRequest) returns (google.longrunning.Operation) { option (google.api.http) = { post: "/v1beta1/echo:wait" @@ -95,7 +154,7 @@ service Echo { }; } - // This method will block (wait) for the requested amount of time + // This method will block (wait) for the requested amount of time // and then return the response or error. // This method showcases how a client handles delays or retries. rpc Block(BlockRequest) returns (BlockResponse) { @@ -106,9 +165,19 @@ service Echo { }; } -// The request message used for the Echo, Collect and Chat methods. If content -// is set in this message then the request will succeed. If status is set in -// this message then the status will be returned as an error. +// A severity enum used to test enum capabilities in GAPIC surfaces. +enum Severity { + UNNECESSARY = 0; + NECESSARY = 1; + URGENT = 2; + CRITICAL = 3; +} + + +// The request message used for the Echo, Collect and Chat methods. +// If content or opt are set in this message then the request will succeed. +// If status is set in this message then the status will be returned as an +// error. message EchoRequest { oneof response { // The content to be echoed by the server. @@ -117,12 +186,24 @@ message EchoRequest { // The error to be thrown by the server. google.rpc.Status error = 2; } + + // The severity to be echoed by the server. + Severity severity = 3; + + // Optional. This field can be set to test the routing annotation on the Echo method. + string header = 4; + + // Optional. This field can be set to test the routing annotation on the Echo method. + string other_header = 5; } // The response message for the Echo methods. message EchoResponse { // The content specified in the request. string content = 1; + + // The severity specified in the request. + Severity severity = 2; } // The request message for the Expand method. @@ -132,6 +213,9 @@ message ExpandRequest { // The error that is thrown after all words are sent on the stream. google.rpc.Status error = 2; + + //The wait time between each server streaming messages + google.protobuf.Duration stream_wait_time = 3; } // The request for the PagedExpand method. @@ -139,13 +223,29 @@ message PagedExpandRequest { // The string to expand. string content = 1 [(google.api.field_behavior) = REQUIRED]; - // The amount of words to returned in each page. + // The number of words to returned in each page. int32 page_size = 2; // The position of the page to be returned. string page_token = 3; } +// The request for the PagedExpandLegacy method. This is a pattern used by some legacy APIs. New +// APIs should NOT use this pattern, but rather something like PagedExpandRequest which conforms to +// aip.dev/158. +message PagedExpandLegacyRequest { + // The string to expand. + string content = 1 [(google.api.field_behavior) = REQUIRED]; + + // The number of words to returned in each page. + // (-- aip.dev/not-precedent: This is a legacy, non-standard pattern that + // violates aip.dev/158. Ordinarily, this should be page_size. --) + int32 max_results = 2; + + // The position of the page to be returned. + string page_token = 3; +} + // The response for the PagedExpand method. message PagedExpandResponse { // The words that were expanded. @@ -155,6 +255,21 @@ message PagedExpandResponse { string next_page_token = 2; } +// A list of words. +message PagedExpandResponseList { + repeated string words = 1; +} + +message PagedExpandLegacyMappedResponse { + // The words that were expanded, indexed by their initial character. + // (-- aip.dev/not-precedent: This is a legacy, non-standard pattern that violates + // aip.dev/158. Ordinarily, this should be a `repeated` field, as in PagedExpandResponse. --) + map alphabetized = 1; + + // The next page token. + string next_page_token = 2; +} + // The request for Wait method. message WaitRequest { oneof end { diff --git a/gax/test/showcase-echo-client/protos/google/showcase/v1beta1/sequence.proto b/gax/test/showcase-echo-client/protos/google/showcase/v1beta1/sequence.proto new file mode 100644 index 000000000..4c2a6bbe0 --- /dev/null +++ b/gax/test/showcase-echo-client/protos/google/showcase/v1beta1/sequence.proto @@ -0,0 +1,258 @@ +// Copyright 2020 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 +// +// https://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. + +syntax = "proto3"; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +package google.showcase.v1beta1; + +option go_package = "github.com/googleapis/gapic-showcase/server/genproto"; +option java_package = "com.google.showcase.v1beta1"; +option java_multiple_files = true; +option ruby_package = "Google::Showcase::V1beta1"; + +service SequenceService { + // This service is meant to only run locally on the port 7469 (keypad digits + // for "show"). + option (google.api.default_host) = "localhost:7469"; + + // Creates a sequence. + rpc CreateSequence(CreateSequenceRequest) returns (Sequence) { + option (google.api.http) = { + post: "/v1beta1/sequences" + body: "sequence" + }; + option (google.api.method_signature) = "sequence"; + }; + + // Creates a sequence. + rpc CreateStreamingSequence(CreateStreamingSequenceRequest) returns (StreamingSequence) { + option (google.api.http) = { + post: "/v1beta1/streamingSequences" + body: "streaming_sequence" + }; + option (google.api.method_signature) = "streaming_sequence"; + }; + + // Retrieves a sequence. + rpc GetSequenceReport(GetSequenceReportRequest) returns (SequenceReport) { + option (google.api.http) = { + get: "/v1beta1/{name=sequences/*/sequenceReport}" + }; + option (google.api.method_signature) = "name"; + }; + + // Retrieves a sequence. + rpc GetStreamingSequenceReport(GetStreamingSequenceReportRequest) returns (StreamingSequenceReport) { + option (google.api.http) = { + get: "/v1beta1/{name=streamingSequences/*/streamingSequenceReport}" + }; + option (google.api.method_signature) = "name"; + }; + + // Attempts a sequence. + rpc AttemptSequence(AttemptSequenceRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/v1beta1/{name=sequences/*}" + body: "*" + }; + option (google.api.method_signature) = "name"; + }; + + // Attempts a streaming sequence. + rpc AttemptStreamingSequence(AttemptStreamingSequenceRequest) returns (stream AttemptStreamingSequenceResponse) { + option (google.api.http) = { + post: "/v1beta1/{name=streamingSequences/*}:stream" + body: "*" + }; + option (google.api.method_signature) = "name"; + }; +} + +message Sequence { + option (google.api.resource) = { + type: "showcase.googleapis.com/Sequence" + pattern: "sequences/{sequence}" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // A server response to an RPC Attempt in a sequence. + message Response { + // The status to return for an individual attempt. + google.rpc.Status status = 1; + + // The amount of time to delay sending the response. + google.protobuf.Duration delay = 2; + } + + // Sequence of responses to return in order for each attempt. If empty, the + // default response is an immediate OK. + repeated Response responses = 2; +} + +message StreamingSequence { + option (google.api.resource) = { + type: "showcase.googleapis.com/StreamingSequence" + pattern: "streamingSequences/{streaming_sequence}" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The Content that the stream will send + string content = 2; + + // A server response to an RPC Attempt in a sequence. + message Response { + // The status to return for an individual attempt. + google.rpc.Status status = 1; + + // The amount of time to delay sending the response. + google.protobuf.Duration delay = 2; + + // The index that the status should be sent + int32 response_index = 3; + } + + // Sequence of responses to return in order for each attempt. If empty, the + // default response is an immediate OK. + repeated Response responses = 3; +} + + +message StreamingSequenceReport { + option (google.api.resource) = { + type: "showcase.googleapis.com/StreamingSequenceReport" + pattern: "streamingSequences/{streaming_sequence}/streamingSequenceReport" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Contains metrics on individual RPC Attempts in a sequence. + message Attempt { + // The attempt number - starting at 0. + int32 attempt_number = 1; + + // The deadline dictated by the attempt to the server. + google.protobuf.Timestamp attempt_deadline = 2; + + // The time that the server responded to the RPC attempt. Used for + // calculating attempt_delay. + google.protobuf.Timestamp response_time = 3; + + // The server perceived delay between sending the last response and + // receiving this attempt. Used for validating attempt delay backoff. + google.protobuf.Duration attempt_delay = 4; + + // The status returned to the attempt. + google.rpc.Status status = 5; + + } + + // The set of RPC attempts received by the server for a Sequence. + repeated Attempt attempts = 2; +} + +message SequenceReport { + option (google.api.resource) = { + type: "showcase.googleapis.com/SequenceReport" + pattern: "sequences/{sequence}/sequenceReport" + }; + + string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Contains metrics on individual RPC Attempts in a sequence. + message Attempt { + // The attempt number - starting at 0. + int32 attempt_number = 1; + + // The deadline dictated by the attempt to the server. + google.protobuf.Timestamp attempt_deadline = 2; + + // The time that the server responded to the RPC attempt. Used for + // calculating attempt_delay. + google.protobuf.Timestamp response_time = 3; + + // The server perceived delay between sending the last response and + // receiving this attempt. Used for validating attempt delay backoff. + google.protobuf.Duration attempt_delay = 4; + + // The status returned to the attempt. + google.rpc.Status status = 5; + } + + // The set of RPC attempts received by the server for a Sequence. + repeated Attempt attempts = 2; +} + +message CreateSequenceRequest { + Sequence sequence = 1; +} + +message CreateStreamingSequenceRequest { + StreamingSequence streaming_sequence = 1; +} + +message AttemptSequenceRequest { + string name = 1 [ + (google.api.resource_reference).type = "showcase.googleapis.com/Sequence", + (google.api.field_behavior) = REQUIRED + ]; + +} + +message AttemptStreamingSequenceRequest { + string name = 1 [ + (google.api.resource_reference).type = "showcase.googleapis.com/StreamingSequence", + (google.api.field_behavior) = REQUIRED + ]; + + // used to send the index of the last failed message + // in the string "content" of an AttemptStreamingSequenceResponse + // needed for stream resumption logic testing + int32 last_fail_index = 2 [ + (google.api.field_behavior) = OPTIONAL + ]; +} + +// The response message for the Echo methods. +message AttemptStreamingSequenceResponse { + // The content specified in the request. + string content = 1; + +} + +message GetSequenceReportRequest { + string name = 1 [ + (google.api.resource_reference).type = + "showcase.googleapis.com/SequenceReport", + (google.api.field_behavior) = REQUIRED + ]; +} + +message GetStreamingSequenceReportRequest { + string name = 1 [ + (google.api.resource_reference).type = + "showcase.googleapis.com/StreamingSequenceReport", + (google.api.field_behavior) = REQUIRED + ]; +} diff --git a/gax/test/showcase-echo-client/src/index.ts b/gax/test/showcase-echo-client/src/index.ts index cfccbdf64..3e3fba4b1 100644 --- a/gax/test/showcase-echo-client/src/index.ts +++ b/gax/test/showcase-echo-client/src/index.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,10 +19,9 @@ import * as v1beta1 from './v1beta1'; const EchoClient = v1beta1.EchoClient; type EchoClient = v1beta1.EchoClient; -export {v1beta1, EchoClient}; -export default { - v1beta1, - EchoClient, -}; +const SequenceServiceClient = v1beta1.SequenceServiceClient; +type SequenceServiceClient = v1beta1.SequenceServiceClient; +export {v1beta1, EchoClient, SequenceServiceClient}; +export default {v1beta1, EchoClient, SequenceServiceClient}; import * as protos from '../protos/protos'; export {protos}; diff --git a/gax/test/showcase-echo-client/src/v1beta1/echo_client.ts b/gax/test/showcase-echo-client/src/v1beta1/echo_client.ts index 92e785460..856d308a2 100644 --- a/gax/test/showcase-echo-client/src/v1beta1/echo_client.ts +++ b/gax/test/showcase-echo-client/src/v1beta1/echo_client.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ // ** All changes to this file may be overwritten. ** /* global window */ -import * as gax from 'google-gax'; -import { +import type * as gax from 'google-gax'; +import type { Callback, CallOptions, Descriptors, @@ -27,16 +27,12 @@ import { LROperation, PaginationCallback, GaxCall, - GoogleError, IamClient, IamProtos, LocationsClient, LocationProtos, } from 'google-gax'; - -import {Transform} from 'stream'; -import {RequestType} from 'google-gax/build/src/apitypes'; -import {PassThrough} from 'stream'; +import {Transform, PassThrough} from 'stream'; import * as protos from '../../protos/protos'; import jsonProtos = require('../../protos/protos.json'); /** @@ -45,7 +41,6 @@ import jsonProtos = require('../../protos/protos.json'); * This file defines retry strategy and timeouts for all API methods in this library. */ import * as gapicConfig from './echo_client_config.json'; -import {operationsProtos} from 'google-gax'; const version = require('../../../package.json').version; /** @@ -53,7 +48,9 @@ const version = require('../../../package.json').version; * side streaming, client side streaming, and bidirectional streaming. This * service also exposes methods that explicitly implement server delay, and * paginated calls. Set the 'showcase-trailer' metadata key on any method - * to have the values echoed in the response trailers. + * to have the values echoed in the response trailers. Set the + * 'x-goog-request-params' metadata key on any method to have the values + * echoed in the response headers. * @class * @memberof v1beta1 */ @@ -108,12 +105,21 @@ export class EchoClient { * API remote host. * @param {gax.ClientConfig} [options.clientConfig] - Client configuration override. * Follows the structure of {@link gapicConfig}. - * @param {boolean | "rest"} [options.fallback] - Use HTTP fallback mode. - * Pass "rest" to use HTTP/1.1 REST API instead of gRPC. + * @param {boolean} [options.fallback] - Use HTTP/1.1 REST mode. * For more information, please check the * {@link https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md#http11-rest-api-mode documentation}. + * @param {gax} [gaxInstance]: loaded instance of `google-gax`. Useful if you + * need to avoid loading the default gRPC version and want to use the fallback + * HTTP implementation. Load only fallback version and pass it to the constructor: + * ``` + * const gax = require('google-gax/build/src/fallback'); // avoids loading google-gax with gRPC + * const client = new EchoClient({fallback: true}, gax); + * ``` */ - constructor(opts?: ClientOptions) { + constructor( + opts?: ClientOptions, + gaxInstance?: typeof gax | typeof gax.fallback + ) { // Ensure that options include all the required fields. const staticMembers = this.constructor as typeof EchoClient; const servicePath = @@ -133,8 +139,13 @@ export class EchoClient { opts['scopes'] = staticMembers.scopes; } + // Load google-gax module synchronously if needed + if (!gaxInstance) { + gaxInstance = require('google-gax') as typeof gax; + } + // Choose either gRPC or proto-over-HTTP implementation of google-gax. - this._gaxModule = opts.fallback ? gax.fallback : gax; + this._gaxModule = opts.fallback ? gaxInstance.fallback : gaxInstance; // Create a `gaxGrpc` object, with any grpc-specific options sent to the client. this._gaxGrpc = new this._gaxModule.GrpcClient(opts); @@ -155,9 +166,12 @@ export class EchoClient { if (servicePath === staticMembers.servicePath) { this.auth.defaultScopes = staticMembers.scopes; } - this.iamClient = new IamClient(this._gaxGrpc, opts); + this.iamClient = new this._gaxModule.IamClient(this._gaxGrpc, opts); - this.locationsClient = new LocationsClient(this._gaxGrpc, opts); + this.locationsClient = new this._gaxModule.LocationsClient( + this._gaxGrpc, + opts + ); // Determine the client header string. const clientHeader = [`gax/${this._gaxModule.version}`, `gapic/${version}`]; @@ -166,10 +180,10 @@ export class EchoClient { } else { clientHeader.push(`gl-web/${this._gaxModule.version}`); } - if (opts.fallback) { - clientHeader.push(`rest/${this._gaxGrpc.grpcVersion}`); - } else { + if (!opts.fallback) { clientHeader.push(`grpc/${this._gaxGrpc.grpcVersion}`); + } else { + clientHeader.push(`rest/${this._gaxGrpc.grpcVersion}`); } if (opts.libName && opts.libVersion) { clientHeader.push(`${opts.libName}/${opts.libVersion}`); @@ -181,31 +195,18 @@ export class EchoClient { // identifiers to uniquely identify resources within the API. // Create useful helper objects for these. this.pathTemplates = { - blueprintPathTemplate: new this._gaxModule.PathTemplate( - 'sessions/{session}/tests/{test}/blueprints/{blueprint}' - ), - roomPathTemplate: new this._gaxModule.PathTemplate('rooms/{room_id}'), - roomIdBlurbIdPathTemplate: new this._gaxModule.PathTemplate( - 'rooms/{room_id}/blurbs/{blurb_id}' + sequencePathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}' ), - roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate: - new this._gaxModule.PathTemplate( - 'rooms/{room_id}/blurbs/legacy/{legacy_room_id}.{blurb_id}' - ), - sessionPathTemplate: new this._gaxModule.PathTemplate( - 'sessions/{session}' + sequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}/sequenceReport' ), - testPathTemplate: new this._gaxModule.PathTemplate( - 'sessions/{session}/tests/{test}' + streamingSequencePathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}' ), - userPathTemplate: new this._gaxModule.PathTemplate('users/{user_id}'), - userIdProfileBlurbIdPathTemplate: new this._gaxModule.PathTemplate( - 'user/{user_id}/profile/blurbs/{blurb_id}' + streamingSequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}/streamingSequenceReport' ), - userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate: - new this._gaxModule.PathTemplate( - 'user/{user_id}/profile/blurbs/legacy/{legacy_user_id}~{blurb_id}' - ), }; // Some of the methods on this service return "paged" results, @@ -223,19 +224,17 @@ export class EchoClient { // Provide descriptors for these. this.descriptors.stream = { expand: new this._gaxModule.StreamDescriptor( - gax.StreamType.SERVER_STREAMING, - // legacy: opts.fallback can be a string or a boolean - opts.fallback ? true : false + this._gaxModule.StreamType.SERVER_STREAMING, + !!opts.fallback, + this._opts.gaxServerStreamingRetries ), collect: new this._gaxModule.StreamDescriptor( - gax.StreamType.CLIENT_STREAMING, - // legacy: opts.fallback can be a string or a boolean - opts.fallback ? true : false + this._gaxModule.StreamType.CLIENT_STREAMING, + !!opts.fallback ), chat: new this._gaxModule.StreamDescriptor( - gax.StreamType.BIDI_STREAMING, - // legacy: opts.fallback can be a string or a boolean - opts.fallback ? true : false + this._gaxModule.StreamType.BIDI_STREAMING, + !!opts.fallback ), }; @@ -346,7 +345,7 @@ export class EchoClient { this.innerApiCalls = {}; // Add a warn function to the client constructor so it can be easily tested. - this.warn = gax.warn; + this.warn = this._gaxModule.warn; } /** @@ -387,6 +386,7 @@ export class EchoClient { 'collect', 'chat', 'pagedExpand', + 'pagedExpandLegacy', 'wait', 'block', ]; @@ -400,7 +400,9 @@ export class EchoClient { setImmediate(() => { stream.emit( 'error', - new GoogleError('The client has already been closed.') + new this._gaxModule.GoogleError( + 'The client has already been closed.' + ) ); }); return stream; @@ -493,12 +495,17 @@ export class EchoClient { * The content to be echoed by the server. * @param {google.rpc.Status} request.error * The error to be thrown by the server. + * @param {google.showcase.v1beta1.Severity} request.severity + * The severity to be echoed by the server. + * @param {string} request.header + * Optional. This field can be set to test the routing annotation on the Echo method. + * @param {string} request.otherHeader + * Optional. This field can be set to test the routing annotation on the Echo method. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods) + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.echo.js * region_tag:localhost_v1beta1_generated_Echo_Echo_async @@ -562,9 +569,202 @@ export class EchoClient { options = options || {}; options.otherArgs = options.otherArgs || {}; options.otherArgs.headers = options.otherArgs.headers || {}; + const routingParameter = {}; + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue.toString().match(RegExp('(?
.*)')); + if (match) { + const parameterValue = match.groups?.['header'] ?? fieldValue; + Object.assign(routingParameter, {header: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?(?:.*)?)')); + if (match) { + const parameterValue = match.groups?.['routing_id'] ?? fieldValue; + Object.assign(routingParameter, {routing_id: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?regions/[^/]+/zones/[^/]+(?:/.*)?)')); + if (match) { + const parameterValue = match.groups?.['table_name'] ?? fieldValue; + Object.assign(routingParameter, {table_name: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?projects/[^/]+)(?:/.*)?')); + if (match) { + const parameterValue = match.groups?.['super_id'] ?? fieldValue; + Object.assign(routingParameter, {super_id: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match( + RegExp('(?projects/[^/]+/instances/[^/]+(?:/.*)?)') + ); + if (match) { + const parameterValue = match.groups?.['table_name'] ?? fieldValue; + Object.assign(routingParameter, {table_name: parameterValue}); + } + } + } + { + const fieldValue = request.header; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match( + RegExp('projects/[^/]+/(?instances/[^/]+)(?:/.*)?') + ); + if (match) { + const parameterValue = match.groups?.['instance_id'] ?? fieldValue; + Object.assign(routingParameter, {instance_id: parameterValue}); + } + } + } + { + const fieldValue = request.otherHeader; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue.toString().match(RegExp('(?(?:.*)?)')); + if (match) { + const parameterValue = match.groups?.['baz'] ?? fieldValue; + Object.assign(routingParameter, {baz: parameterValue}); + } + } + } + { + const fieldValue = request.otherHeader; + if (fieldValue !== undefined && fieldValue !== null) { + const match = fieldValue + .toString() + .match(RegExp('(?projects/[^/]+)(?:/.*)?')); + if (match) { + const parameterValue = match.groups?.['qux'] ?? fieldValue; + Object.assign(routingParameter, {qux: parameterValue}); + } + } + } + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams(routingParameter); this.initialize(); return this.innerApiCalls.echo(request, options, callback); } + /** + * This is similar to the PagedExpand except that it uses + * max_results instead of page_size, as some legacy APIs still + * do. New APIs should NOT use this pattern. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.content + * The string to expand. + * @param {number} request.maxResults + * The number of words to returned in each page. + * (-- aip.dev/not-precedent: This is a legacy, non-standard pattern that + * violates aip.dev/158. Ordinarily, this should be page_size. --) + * @param {string} request.pageToken + * The position of the page to be returned. + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.PagedExpandResponse|PagedExpandResponse}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/echo.paged_expand_legacy.js + * region_tag:localhost_v1beta1_generated_Echo_PagedExpandLegacy_async + */ + pagedExpandLegacy( + request?: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.IPagedExpandResponse, + protos.google.showcase.v1beta1.IPagedExpandLegacyRequest | undefined, + {} | undefined, + ] + >; + pagedExpandLegacy( + request: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + > + ): void; + pagedExpandLegacy( + request: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + callback: Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + > + ): void; + pagedExpandLegacy( + request?: protos.google.showcase.v1beta1.IPagedExpandLegacyRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.IPagedExpandResponse, + | protos.google.showcase.v1beta1.IPagedExpandLegacyRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.IPagedExpandResponse, + protos.google.showcase.v1beta1.IPagedExpandLegacyRequest | undefined, + {} | undefined, + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + this.initialize(); + return this.innerApiCalls.pagedExpandLegacy(request, options, callback); + } /** * This method will block (wait) for the requested amount of time * and then return the response or error. @@ -582,9 +782,8 @@ export class EchoClient { * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [BlockResponse]{@link google.showcase.v1beta1.BlockResponse}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods) + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.BlockResponse|BlockResponse}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.block.js * region_tag:localhost_v1beta1_generated_Echo_Block_async @@ -653,8 +852,8 @@ export class EchoClient { } /** - * This method split the given content into words and will pass each word back - * through the stream. This method showcases server-side streaming rpcs. + * This method splits the given content into words and will pass each word back + * through the stream. This method showcases server-side streaming RPCs. * * @param {Object} request * The request object that will be sent. @@ -662,12 +861,13 @@ export class EchoClient { * The content that will be split into words and returned on the stream. * @param {google.rpc.Status} request.error * The error that is thrown after all words are sent on the stream. + * @param {google.protobuf.Duration} request.streamWaitTime + * The wait time between each server streaming messages * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} - * An object stream which emits [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#server-streaming) + * An object stream which emits {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse} on 'data' event. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#server-streaming | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.expand.js * region_tag:localhost_v1beta1_generated_Echo_Expand_async @@ -687,14 +887,13 @@ export class EchoClient { /** * This method will collect the words given to it. When the stream is closed * by the client, this method will return the a concatenation of the strings - * passed to it. This method showcases client-side streaming rpcs. + * passed to it. This method showcases client-side streaming RPCs. * * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} - A writable stream which accepts objects representing - * [EchoRequest]{@link google.showcase.v1beta1.EchoRequest}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#client-streaming) + * {@link protos.google.showcase.v1beta1.EchoRequest|EchoRequest}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#client-streaming | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.collect.js * region_tag:localhost_v1beta1_generated_Echo_Collect_async @@ -738,18 +937,17 @@ export class EchoClient { } /** - * This method, upon receiving a request on the stream, the same content will - * be passed back on the stream. This method showcases bidirectional - * streaming rpcs. + * This method, upon receiving a request on the stream, will pass the same + * content back on the stream. This method showcases bidirectional + * streaming RPCs. * * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} * An object stream which is both readable and writable. It accepts objects - * representing [EchoRequest]{@link google.showcase.v1beta1.EchoRequest} for write() method, and - * will emit objects representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event asynchronously. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#bi-directional-streaming) + * representing {@link protos.google.showcase.v1beta1.EchoRequest|EchoRequest} for write() method, and + * will emit objects representing {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse} on 'data' event asynchronously. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#bi-directional-streaming | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.chat.js * region_tag:localhost_v1beta1_generated_Echo_Chat_async @@ -760,8 +958,8 @@ export class EchoClient { } /** - * This method will wait the requested amount of and then return. - * This method showcases how a client handles a request timing out. + * This method will wait for the requested amount of time and then return. + * This method showcases how a client handles a request timeout. * * @param {Object} request * The request object that will be sent. @@ -780,8 +978,7 @@ export class EchoClient { * The first element of the array is an object representing * a long running operation. Its `promise()` method returns a promise * you can `await` for. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.wait.js * region_tag:localhost_v1beta1_generated_Echo_Wait_async @@ -872,8 +1069,7 @@ export class EchoClient { * The operation name that will be passed. * @returns {Promise} - The promise which resolves to an object. * The decoded operation object has result and metadata field to get information from. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#long-running-operations | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.wait.js * region_tag:localhost_v1beta1_generated_Echo_Wait_async @@ -886,11 +1082,12 @@ export class EchoClient { protos.google.showcase.v1beta1.WaitMetadata > > { - const request = new operationsProtos.google.longrunning.GetOperationRequest( - {name} - ); + const request = + new this._gaxModule.operationsProtos.google.longrunning.GetOperationRequest( + {name} + ); const [operation] = await this.operationsClient.getOperation(request); - const decodeOperation = new gax.Operation( + const decodeOperation = new this._gaxModule.Operation( operation, this.descriptors.longrunning.wait, this._gaxModule.createDefaultBackoffSettings() @@ -909,20 +1106,19 @@ export class EchoClient { * @param {string} request.content * The string to expand. * @param {number} request.pageSize - * The amount of words to returned in each page. + * The number of words to returned in each page. * @param {string} request.pageToken * The position of the page to be returned. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is Array of [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. + * The first element of the array is Array of {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse}. * The client library will perform auto-pagination by default: it will call the API as many * times as needed and will merge results from all the pages into this array. * Note that it can affect your quota. * We recommend using `pagedExpandAsync()` * method described below for async iteration which you can stop as needed. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. */ pagedExpand( @@ -997,19 +1193,18 @@ export class EchoClient { * @param {string} request.content * The string to expand. * @param {number} request.pageSize - * The amount of words to returned in each page. + * The number of words to returned in each page. * @param {string} request.pageToken * The position of the page to be returned. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} - * An object stream which emits an object representing [EchoResponse]{@link google.showcase.v1beta1.EchoResponse} on 'data' event. + * An object stream which emits an object representing {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse} on 'data' event. * The client library will perform auto-pagination by default: it will call the API as many * times as needed. Note that it can affect your quota. * We recommend using `pagedExpandAsync()` * method described below for async iteration which you can stop as needed. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. */ pagedExpandStream( @@ -1024,7 +1219,7 @@ export class EchoClient { const callSettings = defaultCallSettings.merge(options); this.initialize(); return this.descriptors.page.pagedExpand.createStream( - this.innerApiCalls.pagedExpand as gax.GaxCall, + this.innerApiCalls.pagedExpand as GaxCall, request, callSettings ); @@ -1039,18 +1234,17 @@ export class EchoClient { * @param {string} request.content * The string to expand. * @param {number} request.pageSize - * The amount of words to returned in each page. + * The number of words to returned in each page. * @param {string} request.pageToken * The position of the page to be returned. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} - * An iterable Object that allows [async iteration](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols). + * An iterable Object that allows {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | async iteration }. * When you iterate the returned iterable, each element will be an object representing - * [EchoResponse]{@link google.showcase.v1beta1.EchoResponse}. The API will be called under the hood as needed, once per the page, + * {@link protos.google.showcase.v1beta1.EchoResponse|EchoResponse}. The API will be called under the hood as needed, once per the page, * so you can stop the iteration when you don't need more results. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. * @example include:samples/generated/v1beta1/echo.paged_expand.js * region_tag:localhost_v1beta1_generated_Echo_PagedExpand_async @@ -1068,7 +1262,7 @@ export class EchoClient { this.initialize(); return this.descriptors.page.pagedExpand.asyncIterate( this.innerApiCalls['pagedExpand'] as GaxCall, - request as unknown as RequestType, + request as {}, callSettings ) as AsyncIterable; } @@ -1085,16 +1279,16 @@ export class EchoClient { * OPTIONAL: A `GetPolicyOptions` object for specifying options to * `GetIamPolicy`. This field is only used by Cloud IAM. * - * This object should have the same structure as [GetPolicyOptions]{@link google.iam.v1.GetPolicyOptions} + * This object should have the same structure as {@link google.iam.v1.GetPolicyOptions | GetPolicyOptions}. * @param {Object} [options] * Optional parameters. You can override the default settings for this call, e.g, timeout, - * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html} for the details. + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. * @param {function(?Error, ?Object)} [callback] * The function which will be called with the result of the API call. * - * The second parameter to the callback is an object representing [Policy]{@link google.iam.v1.Policy}. + * The second parameter to the callback is an object representing {@link google.iam.v1.Policy | Policy}. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [Policy]{@link google.iam.v1.Policy}. + * The first element of the array is an object representing {@link google.iam.v1.Policy | Policy}. * The promise has a method named "cancel" which cancels the ongoing API call. */ getIamPolicy( @@ -1132,17 +1326,16 @@ export class EchoClient { * @param {string[]} request.permissions * The set of permissions to check for the `resource`. Permissions with * wildcards (such as '*' or 'storage.*') are not allowed. For more - * information see - * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. * @param {Object} [options] * Optional parameters. You can override the default settings for this call, e.g, timeout, - * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html} for the details. + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. * @param {function(?Error, ?Object)} [callback] * The function which will be called with the result of the API call. * - * The second parameter to the callback is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * The promise has a method named "cancel" which cancels the ongoing API call. */ setIamPolicy( @@ -1180,17 +1373,16 @@ export class EchoClient { * @param {string[]} request.permissions * The set of permissions to check for the `resource`. Permissions with * wildcards (such as '*' or 'storage.*') are not allowed. For more - * information see - * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. * @param {Object} [options] * Optional parameters. You can override the default settings for this call, e.g, timeout, - * retries, paginations, etc. See [gax.CallOptions]{@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html} for the details. + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. * @param {function(?Error, ?Object)} [callback] * The function which will be called with the result of the API call. * - * The second parameter to the callback is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [TestIamPermissionsResponse]{@link google.iam.v1.TestIamPermissionsResponse}. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. * The promise has a method named "cancel" which cancels the ongoing API call. * */ @@ -1220,11 +1412,10 @@ export class EchoClient { * @param {string} request.name * Resource name for the location. * @param {object} [options] - * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html | CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. - * The first element of the array is an object representing [Location]{@link google.cloud.location.Location}. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods) + * The first element of the array is an object representing {@link google.cloud.location.Location | Location}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } * for more details and examples. * @example * ``` @@ -1270,12 +1461,11 @@ export class EchoClient { * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} - * An iterable Object that allows [async iteration](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols). + * An iterable Object that allows {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | async iteration }. * When you iterate the returned iterable, each element will be an object representing - * [Location]{@link google.cloud.location.Location}. The API will be called under the hood as needed, once per the page, + * {@link google.cloud.location.Location | Location}. The API will be called under the hood as needed, once per the page, * so you can stop the iteration when you don't need more results. - * Please see the - * [documentation](https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination) + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } * for more details and examples. * @example * ``` @@ -1301,20 +1491,18 @@ export class EchoClient { * @param {string} request.name - The name of the operation resource. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the - * details. + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} + * for the details. * @param {function(?Error, ?Object)=} callback * The function which will be called with the result of the API call. * * The second parameter to the callback is an object representing - * [google.longrunning.Operation]{@link - * external:"google.longrunning.Operation"}. + * {@link google.longrunning.Operation | google.longrunning.Operation}. * @return {Promise} - The promise which resolves to an array. * The first element of the array is an object representing - * [google.longrunning.Operation]{@link - * external:"google.longrunning.Operation"}. The promise has a method named - * "cancel" which cancels the ongoing API call. + * {@link google.longrunning.Operation | google.longrunning.Operation}. + * The promise has a method named "cancel" which cancels the ongoing API call. * * @example * ``` @@ -1358,11 +1546,11 @@ export class EchoClient { * resources in a page. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} for the * details. * @returns {Object} - * An iterable Object that conforms to @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols. + * An iterable Object that conforms to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | iteration protocols}. * * @example * ``` @@ -1393,8 +1581,8 @@ export class EchoClient { * @param {string} request.name - The name of the operation resource to be cancelled. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} for the * details. * @param {function(?Error)=} callback * The function which will be called with the result of the API call. @@ -1436,9 +1624,9 @@ export class EchoClient { * @param {string} request.name - The name of the operation resource to be deleted. * @param {Object=} options * Optional parameters. You can override the default settings for this call, - * e.g, timeout, retries, paginations, etc. See [gax.CallOptions]{@link - * https://googleapis.github.io/gax-nodejs/global.html#CallOptions} for the - * details. + * e.g, timeout, retries, paginations, etc. See {@link + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions | gax.CallOptions} + * for the details. * @param {function(?Error)=} callback * The function which will be called with the result of the API call. * @return {Promise} - The promise which resolves when API call finishes. @@ -1474,371 +1662,105 @@ export class EchoClient { // -------------------- /** - * Return a fully-qualified blueprint resource name string. + * Return a fully-qualified sequence resource name string. * - * @param {string} session - * @param {string} test - * @param {string} blueprint + * @param {string} sequence * @returns {string} Resource name string. */ - blueprintPath(session: string, test: string, blueprint: string) { - return this.pathTemplates.blueprintPathTemplate.render({ - session: session, - test: test, - blueprint: blueprint, + sequencePath(sequence: string) { + return this.pathTemplates.sequencePathTemplate.render({ + sequence: sequence, }); } /** - * Parse the session from Blueprint resource. - * - * @param {string} blueprintName - * A fully-qualified path representing Blueprint resource. - * @returns {string} A string representing the session. - */ - matchSessionFromBlueprintName(blueprintName: string) { - return this.pathTemplates.blueprintPathTemplate.match(blueprintName) - .session; - } - - /** - * Parse the test from Blueprint resource. - * - * @param {string} blueprintName - * A fully-qualified path representing Blueprint resource. - * @returns {string} A string representing the test. - */ - matchTestFromBlueprintName(blueprintName: string) { - return this.pathTemplates.blueprintPathTemplate.match(blueprintName).test; - } - - /** - * Parse the blueprint from Blueprint resource. + * Parse the sequence from Sequence resource. * - * @param {string} blueprintName - * A fully-qualified path representing Blueprint resource. - * @returns {string} A string representing the blueprint. + * @param {string} sequenceName + * A fully-qualified path representing Sequence resource. + * @returns {string} A string representing the sequence. */ - matchBlueprintFromBlueprintName(blueprintName: string) { - return this.pathTemplates.blueprintPathTemplate.match(blueprintName) - .blueprint; + matchSequenceFromSequenceName(sequenceName: string) { + return this.pathTemplates.sequencePathTemplate.match(sequenceName).sequence; } /** - * Return a fully-qualified room resource name string. + * Return a fully-qualified sequenceReport resource name string. * - * @param {string} room_id + * @param {string} sequence * @returns {string} Resource name string. */ - roomPath(roomId: string) { - return this.pathTemplates.roomPathTemplate.render({ - room_id: roomId, + sequenceReportPath(sequence: string) { + return this.pathTemplates.sequenceReportPathTemplate.render({ + sequence: sequence, }); } /** - * Parse the room_id from Room resource. + * Parse the sequence from SequenceReport resource. * - * @param {string} roomName - * A fully-qualified path representing Room resource. - * @returns {string} A string representing the room_id. + * @param {string} sequenceReportName + * A fully-qualified path representing SequenceReport resource. + * @returns {string} A string representing the sequence. */ - matchRoomIdFromRoomName(roomName: string) { - return this.pathTemplates.roomPathTemplate.match(roomName).room_id; + matchSequenceFromSequenceReportName(sequenceReportName: string) { + return this.pathTemplates.sequenceReportPathTemplate.match( + sequenceReportName + ).sequence; } /** - * Return a fully-qualified roomIdBlurbId resource name string. + * Return a fully-qualified streamingSequence resource name string. * - * @param {string} room_id - * @param {string} blurb_id + * @param {string} streaming_sequence * @returns {string} Resource name string. */ - roomIdBlurbIdPath(roomId: string, blurbId: string) { - return this.pathTemplates.roomIdBlurbIdPathTemplate.render({ - room_id: roomId, - blurb_id: blurbId, + streamingSequencePath(streamingSequence: string) { + return this.pathTemplates.streamingSequencePathTemplate.render({ + streaming_sequence: streamingSequence, }); } /** - * Parse the room_id from RoomIdBlurbId resource. - * - * @param {string} roomIdBlurbIdName - * A fully-qualified path representing room_id_blurb_id resource. - * @returns {string} A string representing the room_id. - */ - matchRoomIdFromRoomIdBlurbIdName(roomIdBlurbIdName: string) { - return this.pathTemplates.roomIdBlurbIdPathTemplate.match(roomIdBlurbIdName) - .room_id; - } - - /** - * Parse the blurb_id from RoomIdBlurbId resource. - * - * @param {string} roomIdBlurbIdName - * A fully-qualified path representing room_id_blurb_id resource. - * @returns {string} A string representing the blurb_id. - */ - matchBlurbIdFromRoomIdBlurbIdName(roomIdBlurbIdName: string) { - return this.pathTemplates.roomIdBlurbIdPathTemplate.match(roomIdBlurbIdName) - .blurb_id; - } - - /** - * Return a fully-qualified roomIdBlurbsLegacyRoomIdBlurbId resource name string. - * - * @param {string} room_id - * @param {string} legacy_room_id - * @param {string} blurb_id - * @returns {string} Resource name string. - */ - roomIdBlurbsLegacyRoomIdBlurbIdPath( - roomId: string, - legacyRoomId: string, - blurbId: string - ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.render( - { - room_id: roomId, - legacy_room_id: legacyRoomId, - blurb_id: blurbId, - } - ); - } - - /** - * Parse the room_id from RoomIdBlurbsLegacyRoomIdBlurbId resource. + * Parse the streaming_sequence from StreamingSequence resource. * - * @param {string} roomIdBlurbsLegacyRoomIdBlurbIdName - * A fully-qualified path representing room_id_blurbs_legacy_room_id_blurb_id resource. - * @returns {string} A string representing the room_id. + * @param {string} streamingSequenceName + * A fully-qualified path representing StreamingSequence resource. + * @returns {string} A string representing the streaming_sequence. */ - matchRoomIdFromRoomIdBlurbsLegacyRoomIdBlurbIdName( - roomIdBlurbsLegacyRoomIdBlurbIdName: string + matchStreamingSequenceFromStreamingSequenceName( + streamingSequenceName: string ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.match( - roomIdBlurbsLegacyRoomIdBlurbIdName - ).room_id; - } - - /** - * Parse the legacy_room_id from RoomIdBlurbsLegacyRoomIdBlurbId resource. - * - * @param {string} roomIdBlurbsLegacyRoomIdBlurbIdName - * A fully-qualified path representing room_id_blurbs_legacy_room_id_blurb_id resource. - * @returns {string} A string representing the legacy_room_id. - */ - matchLegacyRoomIdFromRoomIdBlurbsLegacyRoomIdBlurbIdName( - roomIdBlurbsLegacyRoomIdBlurbIdName: string - ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.match( - roomIdBlurbsLegacyRoomIdBlurbIdName - ).legacy_room_id; - } - - /** - * Parse the blurb_id from RoomIdBlurbsLegacyRoomIdBlurbId resource. - * - * @param {string} roomIdBlurbsLegacyRoomIdBlurbIdName - * A fully-qualified path representing room_id_blurbs_legacy_room_id_blurb_id resource. - * @returns {string} A string representing the blurb_id. - */ - matchBlurbIdFromRoomIdBlurbsLegacyRoomIdBlurbIdName( - roomIdBlurbsLegacyRoomIdBlurbIdName: string - ) { - return this.pathTemplates.roomIdBlurbsLegacyRoomIdBlurbIdPathTemplate.match( - roomIdBlurbsLegacyRoomIdBlurbIdName - ).blurb_id; - } - - /** - * Return a fully-qualified session resource name string. - * - * @param {string} session - * @returns {string} Resource name string. - */ - sessionPath(session: string) { - return this.pathTemplates.sessionPathTemplate.render({ - session: session, - }); - } - - /** - * Parse the session from Session resource. - * - * @param {string} sessionName - * A fully-qualified path representing Session resource. - * @returns {string} A string representing the session. - */ - matchSessionFromSessionName(sessionName: string) { - return this.pathTemplates.sessionPathTemplate.match(sessionName).session; - } - - /** - * Return a fully-qualified test resource name string. - * - * @param {string} session - * @param {string} test - * @returns {string} Resource name string. - */ - testPath(session: string, test: string) { - return this.pathTemplates.testPathTemplate.render({ - session: session, - test: test, - }); - } - - /** - * Parse the session from Test resource. - * - * @param {string} testName - * A fully-qualified path representing Test resource. - * @returns {string} A string representing the session. - */ - matchSessionFromTestName(testName: string) { - return this.pathTemplates.testPathTemplate.match(testName).session; - } - - /** - * Parse the test from Test resource. - * - * @param {string} testName - * A fully-qualified path representing Test resource. - * @returns {string} A string representing the test. - */ - matchTestFromTestName(testName: string) { - return this.pathTemplates.testPathTemplate.match(testName).test; - } - - /** - * Return a fully-qualified user resource name string. - * - * @param {string} user_id - * @returns {string} Resource name string. - */ - userPath(userId: string) { - return this.pathTemplates.userPathTemplate.render({ - user_id: userId, - }); - } - - /** - * Parse the user_id from User resource. - * - * @param {string} userName - * A fully-qualified path representing User resource. - * @returns {string} A string representing the user_id. - */ - matchUserIdFromUserName(userName: string) { - return this.pathTemplates.userPathTemplate.match(userName).user_id; + return this.pathTemplates.streamingSequencePathTemplate.match( + streamingSequenceName + ).streaming_sequence; } /** - * Return a fully-qualified userIdProfileBlurbId resource name string. + * Return a fully-qualified streamingSequenceReport resource name string. * - * @param {string} user_id - * @param {string} blurb_id + * @param {string} streaming_sequence * @returns {string} Resource name string. */ - userIdProfileBlurbIdPath(userId: string, blurbId: string) { - return this.pathTemplates.userIdProfileBlurbIdPathTemplate.render({ - user_id: userId, - blurb_id: blurbId, + streamingSequenceReportPath(streamingSequence: string) { + return this.pathTemplates.streamingSequenceReportPathTemplate.render({ + streaming_sequence: streamingSequence, }); } /** - * Parse the user_id from UserIdProfileBlurbId resource. - * - * @param {string} userIdProfileBlurbIdName - * A fully-qualified path representing user_id_profile_blurb_id resource. - * @returns {string} A string representing the user_id. - */ - matchUserIdFromUserIdProfileBlurbIdName(userIdProfileBlurbIdName: string) { - return this.pathTemplates.userIdProfileBlurbIdPathTemplate.match( - userIdProfileBlurbIdName - ).user_id; - } - - /** - * Parse the blurb_id from UserIdProfileBlurbId resource. - * - * @param {string} userIdProfileBlurbIdName - * A fully-qualified path representing user_id_profile_blurb_id resource. - * @returns {string} A string representing the blurb_id. - */ - matchBlurbIdFromUserIdProfileBlurbIdName(userIdProfileBlurbIdName: string) { - return this.pathTemplates.userIdProfileBlurbIdPathTemplate.match( - userIdProfileBlurbIdName - ).blurb_id; - } - - /** - * Return a fully-qualified userIdProfileBlurbsLegacyUserIdBlurbId resource name string. - * - * @param {string} user_id - * @param {string} legacy_user_id - * @param {string} blurb_id - * @returns {string} Resource name string. - */ - userIdProfileBlurbsLegacyUserIdBlurbIdPath( - userId: string, - legacyUserId: string, - blurbId: string - ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.render( - { - user_id: userId, - legacy_user_id: legacyUserId, - blurb_id: blurbId, - } - ); - } - - /** - * Parse the user_id from UserIdProfileBlurbsLegacyUserIdBlurbId resource. - * - * @param {string} userIdProfileBlurbsLegacyUserIdBlurbIdName - * A fully-qualified path representing user_id_profile_blurbs_legacy_user_id_blurb_id resource. - * @returns {string} A string representing the user_id. - */ - matchUserIdFromUserIdProfileBlurbsLegacyUserIdBlurbIdName( - userIdProfileBlurbsLegacyUserIdBlurbIdName: string - ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.match( - userIdProfileBlurbsLegacyUserIdBlurbIdName - ).user_id; - } - - /** - * Parse the legacy_user_id from UserIdProfileBlurbsLegacyUserIdBlurbId resource. - * - * @param {string} userIdProfileBlurbsLegacyUserIdBlurbIdName - * A fully-qualified path representing user_id_profile_blurbs_legacy_user_id_blurb_id resource. - * @returns {string} A string representing the legacy_user_id. - */ - matchLegacyUserIdFromUserIdProfileBlurbsLegacyUserIdBlurbIdName( - userIdProfileBlurbsLegacyUserIdBlurbIdName: string - ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.match( - userIdProfileBlurbsLegacyUserIdBlurbIdName - ).legacy_user_id; - } - - /** - * Parse the blurb_id from UserIdProfileBlurbsLegacyUserIdBlurbId resource. + * Parse the streaming_sequence from StreamingSequenceReport resource. * - * @param {string} userIdProfileBlurbsLegacyUserIdBlurbIdName - * A fully-qualified path representing user_id_profile_blurbs_legacy_user_id_blurb_id resource. - * @returns {string} A string representing the blurb_id. + * @param {string} streamingSequenceReportName + * A fully-qualified path representing StreamingSequenceReport resource. + * @returns {string} A string representing the streaming_sequence. */ - matchBlurbIdFromUserIdProfileBlurbsLegacyUserIdBlurbIdName( - userIdProfileBlurbsLegacyUserIdBlurbIdName: string + matchStreamingSequenceFromStreamingSequenceReportName( + streamingSequenceReportName: string ) { - return this.pathTemplates.userIdProfileBlurbsLegacyUserIdBlurbIdPathTemplate.match( - userIdProfileBlurbsLegacyUserIdBlurbIdName - ).blurb_id; + return this.pathTemplates.streamingSequenceReportPathTemplate.match( + streamingSequenceReportName + ).streaming_sequence; } /** diff --git a/gax/test/showcase-echo-client/src/v1beta1/echo_client_config.json b/gax/test/showcase-echo-client/src/v1beta1/echo_client_config.json index c30d10733..57bd509e9 100644 --- a/gax/test/showcase-echo-client/src/v1beta1/echo_client_config.json +++ b/gax/test/showcase-echo-client/src/v1beta1/echo_client_config.json @@ -40,6 +40,14 @@ "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, + "PagedExpandLegacy": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "PagedExpandLegacyMapped": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, "Wait": { "retry_codes_name": "non_idempotent", "retry_params_name": "default" diff --git a/gax/test/showcase-echo-client/src/v1beta1/echo_proto_list.json b/gax/test/showcase-echo-client/src/v1beta1/echo_proto_list.json index 4d911013e..76b3da61a 100644 --- a/gax/test/showcase-echo-client/src/v1beta1/echo_proto_list.json +++ b/gax/test/showcase-echo-client/src/v1beta1/echo_proto_list.json @@ -1,3 +1,4 @@ [ - "../../protos/google/showcase/v1beta1/echo.proto" + "../../protos/google/showcase/v1beta1/echo.proto", + "../../protos/google/showcase/v1beta1/sequence.proto" ] diff --git a/gax/test/showcase-echo-client/src/v1beta1/index.ts b/gax/test/showcase-echo-client/src/v1beta1/index.ts index eb1faebc8..be6bb2377 100644 --- a/gax/test/showcase-echo-client/src/v1beta1/index.ts +++ b/gax/test/showcase-echo-client/src/v1beta1/index.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,3 +17,4 @@ // ** All changes to this file may be overwritten. ** export {EchoClient} from './echo_client'; +export {SequenceServiceClient} from './sequence_service_client'; diff --git a/gax/test/showcase-echo-client/src/v1beta1/sequence_service_client.ts b/gax/test/showcase-echo-client/src/v1beta1/sequence_service_client.ts new file mode 100644 index 000000000..a07bc75f6 --- /dev/null +++ b/gax/test/showcase-echo-client/src/v1beta1/sequence_service_client.ts @@ -0,0 +1,1181 @@ +// Copyright 2023 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 +// +// https://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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +/* global window */ +import type * as gax from 'google-gax'; +import type { + Callback, + CallOptions, + Descriptors, + ClientOptions, + IamClient, + IamProtos, + LocationsClient, + LocationProtos, +} from 'google-gax'; +import {PassThrough} from 'stream'; +import * as protos from '../../protos/protos'; +import jsonProtos = require('../../protos/protos.json'); +/** + * Client JSON configuration object, loaded from + * `src/v1beta1/sequence_service_client_config.json`. + * This file defines retry strategy and timeouts for all API methods in this library. + */ +import * as gapicConfig from './sequence_service_client_config.json'; +const version = require('../../../package.json').version; + +/** + * @class + * @memberof v1beta1 + */ +export class SequenceServiceClient { + private _terminated = false; + private _opts: ClientOptions; + private _providedCustomServicePath: boolean; + private _gaxModule: typeof gax | typeof gax.fallback; + private _gaxGrpc: gax.GrpcClient | gax.fallback.GrpcClient; + private _protos: {}; + private _defaults: {[method: string]: gax.CallSettings}; + auth: gax.GoogleAuth; + descriptors: Descriptors = { + page: {}, + stream: {}, + longrunning: {}, + batching: {}, + }; + warn: (code: string, message: string, warnType?: string) => void; + innerApiCalls: {[name: string]: Function}; + iamClient: IamClient; + locationsClient: LocationsClient; + pathTemplates: {[name: string]: gax.PathTemplate}; + sequenceServiceStub?: Promise<{[name: string]: Function}>; + + /** + * Construct an instance of SequenceServiceClient. + * + * @param {object} [options] - The configuration object. + * The options accepted by the constructor are described in detail + * in [this document](https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md#creating-the-client-instance). + * The common options are: + * @param {object} [options.credentials] - Credentials object. + * @param {string} [options.credentials.client_email] + * @param {string} [options.credentials.private_key] + * @param {string} [options.email] - Account email address. Required when + * using a .pem or .p12 keyFilename. + * @param {string} [options.keyFilename] - Full path to the a .json, .pem, or + * .p12 key downloaded from the Google Developers Console. If you provide + * a path to a JSON file, the projectId option below is not necessary. + * NOTE: .pem and .p12 require you to specify options.email as well. + * @param {number} [options.port] - The port on which to connect to + * the remote host. + * @param {string} [options.projectId] - The project ID from the Google + * Developer's Console, e.g. 'grape-spaceship-123'. We will also check + * the environment variable GCLOUD_PROJECT for your project ID. If your + * app is running in an environment which supports + * {@link https://developers.google.com/identity/protocols/application-default-credentials Application Default Credentials}, + * your project ID will be detected automatically. + * @param {string} [options.apiEndpoint] - The domain name of the + * API remote host. + * @param {gax.ClientConfig} [options.clientConfig] - Client configuration override. + * Follows the structure of {@link gapicConfig}. + * @param {boolean} [options.fallback] - Use HTTP/1.1 REST mode. + * For more information, please check the + * {@link https://github.com/googleapis/gax-nodejs/blob/main/client-libraries.md#http11-rest-api-mode documentation}. + * @param {gax} [gaxInstance]: loaded instance of `google-gax`. Useful if you + * need to avoid loading the default gRPC version and want to use the fallback + * HTTP implementation. Load only fallback version and pass it to the constructor: + * ``` + * const gax = require('google-gax/build/src/fallback'); // avoids loading google-gax with gRPC + * const client = new SequenceServiceClient({fallback: true}, gax); + * ``` + */ + constructor( + opts?: ClientOptions, + gaxInstance?: typeof gax | typeof gax.fallback + ) { + // Ensure that options include all the required fields. + const staticMembers = this.constructor as typeof SequenceServiceClient; + const servicePath = + opts?.servicePath || opts?.apiEndpoint || staticMembers.servicePath; + this._providedCustomServicePath = !!( + opts?.servicePath || opts?.apiEndpoint + ); + const port = opts?.port || staticMembers.port; + const clientConfig = opts?.clientConfig ?? {}; + const fallback = + opts?.fallback ?? + (typeof window !== 'undefined' && typeof window?.fetch === 'function'); + opts = Object.assign({servicePath, port, clientConfig, fallback}, opts); + + // If scopes are unset in options and we're connecting to a non-default endpoint, set scopes just in case. + if (servicePath !== staticMembers.servicePath && !('scopes' in opts)) { + opts['scopes'] = staticMembers.scopes; + } + + // Load google-gax module synchronously if needed + if (!gaxInstance) { + gaxInstance = require('google-gax') as typeof gax; + } + + // Choose either gRPC or proto-over-HTTP implementation of google-gax. + this._gaxModule = opts.fallback ? gaxInstance.fallback : gaxInstance; + + // Create a `gaxGrpc` object, with any grpc-specific options sent to the client. + this._gaxGrpc = new this._gaxModule.GrpcClient(opts); + + // Save options to use in initialize() method. + this._opts = opts; + + // Save the auth object to the client, for use by other methods. + this.auth = this._gaxGrpc.auth as gax.GoogleAuth; + + // Set useJWTAccessWithScope on the auth object. + this.auth.useJWTAccessWithScope = true; + + // Set defaultServicePath on the auth object. + this.auth.defaultServicePath = staticMembers.servicePath; + + // Set the default scopes in auth client if needed. + if (servicePath === staticMembers.servicePath) { + this.auth.defaultScopes = staticMembers.scopes; + } + this.iamClient = new this._gaxModule.IamClient(this._gaxGrpc, opts); + + this.locationsClient = new this._gaxModule.LocationsClient( + this._gaxGrpc, + opts + ); + + // Determine the client header string. + const clientHeader = [`gax/${this._gaxModule.version}`, `gapic/${version}`]; + if (typeof process !== 'undefined' && 'versions' in process) { + clientHeader.push(`gl-node/${process.versions.node}`); + } else { + clientHeader.push(`gl-web/${this._gaxModule.version}`); + } + if (!opts.fallback) { + clientHeader.push(`grpc/${this._gaxGrpc.grpcVersion}`); + } else { + clientHeader.push(`rest/${this._gaxGrpc.grpcVersion}`); + } + if (opts.libName && opts.libVersion) { + clientHeader.push(`${opts.libName}/${opts.libVersion}`); + } + // Load the applicable protos. + this._protos = this._gaxGrpc.loadProtoJSON(jsonProtos); + + // This API contains "path templates"; forward-slash-separated + // identifiers to uniquely identify resources within the API. + // Create useful helper objects for these. + this.pathTemplates = { + sequencePathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}' + ), + sequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'sequences/{sequence}/sequenceReport' + ), + streamingSequencePathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}' + ), + streamingSequenceReportPathTemplate: new this._gaxModule.PathTemplate( + 'streamingSequences/{streaming_sequence}/streamingSequenceReport' + ), + }; + + // Some of the methods on this service provide streaming responses. + // Provide descriptors for these. + this.descriptors.stream = { + attemptStreamingSequence: new this._gaxModule.StreamDescriptor( + this._gaxModule.StreamType.SERVER_STREAMING, + !!opts.fallback, + this._opts.gaxServerStreamingRetries + ), + }; + + // Put together the default options sent with requests. + this._defaults = this._gaxGrpc.constructSettings( + 'google.showcase.v1beta1.SequenceService', + gapicConfig as gax.ClientConfig, + opts.clientConfig || {}, + {'x-goog-api-client': clientHeader.join(' ')} + ); + + // Set up a dictionary of "inner API calls"; the core implementation + // of calling the API is handled in `google-gax`, with this code + // merely providing the destination and request information. + this.innerApiCalls = {}; + + // Add a warn function to the client constructor so it can be easily tested. + this.warn = this._gaxModule.warn; + } + + /** + * Initialize the client. + * Performs asynchronous operations (such as authentication) and prepares the client. + * This function will be called automatically when any class method is called for the + * first time, but if you need to initialize it before calling an actual method, + * feel free to call initialize() directly. + * + * You can await on this method if you want to make sure the client is initialized. + * + * @returns {Promise} A promise that resolves to an authenticated service stub. + */ + initialize() { + // If the client stub promise is already initialized, return immediately. + if (this.sequenceServiceStub) { + return this.sequenceServiceStub; + } + + // Put together the "service stub" for + // google.showcase.v1beta1.SequenceService. + this.sequenceServiceStub = this._gaxGrpc.createStub( + this._opts.fallback + ? (this._protos as protobuf.Root).lookupService( + 'google.showcase.v1beta1.SequenceService' + ) + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this._protos as any).google.showcase.v1beta1.SequenceService, + this._opts, + this._providedCustomServicePath + ) as Promise<{[method: string]: Function}>; + + // Iterate over each of the methods that the service provides + // and create an API call method for each. + const sequenceServiceStubMethods = [ + 'createSequence', + 'createStreamingSequence', + 'getSequenceReport', + 'getStreamingSequenceReport', + 'attemptSequence', + 'attemptStreamingSequence', + ]; + for (const methodName of sequenceServiceStubMethods) { + const callPromise = this.sequenceServiceStub.then( + stub => + (...args: Array<{}>) => { + if (this._terminated) { + if (methodName in this.descriptors.stream) { + const stream = new PassThrough(); + setImmediate(() => { + stream.emit( + 'error', + new this._gaxModule.GoogleError( + 'The client has already been closed.' + ) + ); + }); + return stream; + } + return Promise.reject('The client has already been closed.'); + } + const func = stub[methodName]; + return func.apply(stub, args); + }, + (err: Error | null | undefined) => () => { + throw err; + } + ); + + const descriptor = this.descriptors.stream[methodName] || undefined; + const apiCall = this._gaxModule.createApiCall( + callPromise, + this._defaults[methodName], + descriptor, + this._opts.fallback + ); + + this.innerApiCalls[methodName] = apiCall; + } + + return this.sequenceServiceStub; + } + + /** + * The DNS address for this API service. + * @returns {string} The DNS address for this service. + */ + static get servicePath() { + return 'localhost'; + } + + /** + * The DNS address for this API service - same as servicePath(), + * exists for compatibility reasons. + * @returns {string} The DNS address for this service. + */ + static get apiEndpoint() { + return 'localhost'; + } + + /** + * The port for this API service. + * @returns {number} The default port for this service. + */ + static get port() { + return 7469; + } + + /** + * The scopes needed to make gRPC calls for every method defined + * in this service. + * @returns {string[]} List of default scopes. + */ + static get scopes() { + return []; + } + + getProjectId(): Promise; + getProjectId(callback: Callback): void; + /** + * Return the project ID used by this class. + * @returns {Promise} A promise that resolves to string containing the project ID. + */ + getProjectId( + callback?: Callback + ): Promise | void { + if (callback) { + this.auth.getProjectId(callback); + return; + } + return this.auth.getProjectId(); + } + + // ------------------- + // -- Service calls -- + // ------------------- + /** + * Creates a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {google.showcase.v1beta1.Sequence} request.sequence + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.Sequence|Sequence}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.create_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_CreateSequence_async + */ + createSequence( + request?: protos.google.showcase.v1beta1.ICreateSequenceRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | undefined, + {} | undefined, + ] + >; + createSequence( + request: protos.google.showcase.v1beta1.ICreateSequenceRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + createSequence( + request: protos.google.showcase.v1beta1.ICreateSequenceRequest, + callback: Callback< + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + createSequence( + request?: protos.google.showcase.v1beta1.ICreateSequenceRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.ISequence, + | protos.google.showcase.v1beta1.ICreateSequenceRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | null | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.ISequence, + protos.google.showcase.v1beta1.ICreateSequenceRequest | undefined, + {} | undefined, + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + this.initialize(); + return this.innerApiCalls.createSequence(request, options, callback); + } + /** + * Creates a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {google.showcase.v1beta1.StreamingSequence} request.streamingSequence + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.StreamingSequence|StreamingSequence}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.create_streaming_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_CreateStreamingSequence_async + */ + createStreamingSequence( + request?: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequence, + ( + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | undefined + ), + {} | undefined, + ] + >; + createStreamingSequence( + request: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + > + ): void; + createStreamingSequence( + request: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + > + ): void; + createStreamingSequence( + request?: protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.IStreamingSequence, + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequence, + ( + | protos.google.showcase.v1beta1.ICreateStreamingSequenceRequest + | undefined + ), + {} | undefined, + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + this.initialize(); + return this.innerApiCalls.createStreamingSequence( + request, + options, + callback + ); + } + /** + * Retrieves a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.SequenceReport|SequenceReport}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.get_sequence_report.js + * region_tag:localhost_v1beta1_generated_SequenceService_GetSequenceReport_async + */ + getSequenceReport( + request?: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.ISequenceReport, + protos.google.showcase.v1beta1.IGetSequenceReportRequest | undefined, + {} | undefined, + ] + >; + getSequenceReport( + request: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getSequenceReport( + request: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + callback: Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getSequenceReport( + request?: protos.google.showcase.v1beta1.IGetSequenceReportRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.ISequenceReport, + | protos.google.showcase.v1beta1.IGetSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.ISequenceReport, + protos.google.showcase.v1beta1.IGetSequenceReportRequest | undefined, + {} | undefined, + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.getSequenceReport(request, options, callback); + } + /** + * Retrieves a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.showcase.v1beta1.StreamingSequenceReport|StreamingSequenceReport}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.get_streaming_sequence_report.js + * region_tag:localhost_v1beta1_generated_SequenceService_GetStreamingSequenceReport_async + */ + getStreamingSequenceReport( + request?: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + options?: CallOptions + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequenceReport, + ( + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | undefined + ), + {} | undefined, + ] + >; + getStreamingSequenceReport( + request: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + options: CallOptions, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getStreamingSequenceReport( + request: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + callback: Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): void; + getStreamingSequenceReport( + request?: protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.showcase.v1beta1.IStreamingSequenceReport, + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | null + | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.showcase.v1beta1.IStreamingSequenceReport, + ( + | protos.google.showcase.v1beta1.IGetStreamingSequenceReportRequest + | undefined + ), + {} | undefined, + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.getStreamingSequenceReport( + request, + options, + callback + ); + } + /** + * Attempts a sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link protos.google.protobuf.Empty|Empty}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.attempt_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_AttemptSequence_async + */ + attemptSequence( + request?: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + options?: CallOptions + ): Promise< + [ + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | undefined, + {} | undefined, + ] + >; + attemptSequence( + request: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + options: CallOptions, + callback: Callback< + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + attemptSequence( + request: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + callback: Callback< + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | null | undefined, + {} | null | undefined + > + ): void; + attemptSequence( + request?: protos.google.showcase.v1beta1.IAttemptSequenceRequest, + optionsOrCallback?: + | CallOptions + | Callback< + protos.google.protobuf.IEmpty, + | protos.google.showcase.v1beta1.IAttemptSequenceRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | null | undefined, + {} | null | undefined + > + ): Promise< + [ + protos.google.protobuf.IEmpty, + protos.google.showcase.v1beta1.IAttemptSequenceRequest | undefined, + {} | undefined, + ] + > | void { + request = request || {}; + let options: CallOptions; + if (typeof optionsOrCallback === 'function' && callback === undefined) { + callback = optionsOrCallback; + options = {}; + } else { + options = optionsOrCallback as CallOptions; + } + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.attemptSequence(request, options, callback); + } + + /** + * Attempts a streaming sequence. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * @param {number} [request.lastFailIndex] + * used to send the index of the last failed message + * in the string "content" of an AttemptStreamingSequenceResponse + * needed for stream resumption logic testing + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Stream} + * An object stream which emits {@link protos.google.showcase.v1beta1.AttemptStreamingSequenceResponse|AttemptStreamingSequenceResponse} on 'data' event. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#server-streaming | documentation } + * for more details and examples. + * @example include:samples/generated/v1beta1/sequence_service.attempt_streaming_sequence.js + * region_tag:localhost_v1beta1_generated_SequenceService_AttemptStreamingSequence_async + */ + attemptStreamingSequence( + request?: protos.google.showcase.v1beta1.IAttemptStreamingSequenceRequest, + options?: CallOptions + ): gax.CancellableStream { + request = request || {}; + options = options || {}; + options.otherArgs = options.otherArgs || {}; + options.otherArgs.headers = options.otherArgs.headers || {}; + options.otherArgs.headers['x-goog-request-params'] = + this._gaxModule.routingHeader.fromParams({ + name: request.name ?? '', + }); + this.initialize(); + return this.innerApiCalls.attemptStreamingSequence(request, options); + } + + /** + * Gets the access control policy for a resource. Returns an empty policy + * if the resource exists and does not have a policy set. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.resource + * REQUIRED: The resource for which the policy is being requested. + * See the operation documentation for the appropriate value for this field. + * @param {Object} [request.options] + * OPTIONAL: A `GetPolicyOptions` object for specifying options to + * `GetIamPolicy`. This field is only used by Cloud IAM. + * + * This object should have the same structure as {@link google.iam.v1.GetPolicyOptions | GetPolicyOptions}. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing {@link google.iam.v1.Policy | Policy}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.iam.v1.Policy | Policy}. + * The promise has a method named "cancel" which cancels the ongoing API call. + */ + getIamPolicy( + request: IamProtos.google.iam.v1.GetIamPolicyRequest, + options?: + | gax.CallOptions + | Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.GetIamPolicyRequest | null | undefined, + {} | null | undefined + >, + callback?: Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.GetIamPolicyRequest | null | undefined, + {} | null | undefined + > + ): Promise<[IamProtos.google.iam.v1.Policy]> { + return this.iamClient.getIamPolicy(request, options, callback); + } + + /** + * Returns permissions that a caller has on the specified resource. If the + * resource does not exist, this will return an empty set of + * permissions, not a NOT_FOUND error. + * + * Note: This operation is designed to be used for building + * permission-aware UIs and command-line tools, not for authorization + * checking. This operation may "fail open" without warning. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.resource + * REQUIRED: The resource for which the policy detail is being requested. + * See the operation documentation for the appropriate value for this field. + * @param {string[]} request.permissions + * The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * The promise has a method named "cancel" which cancels the ongoing API call. + */ + setIamPolicy( + request: IamProtos.google.iam.v1.SetIamPolicyRequest, + options?: + | gax.CallOptions + | Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.SetIamPolicyRequest | null | undefined, + {} | null | undefined + >, + callback?: Callback< + IamProtos.google.iam.v1.Policy, + IamProtos.google.iam.v1.SetIamPolicyRequest | null | undefined, + {} | null | undefined + > + ): Promise<[IamProtos.google.iam.v1.Policy]> { + return this.iamClient.setIamPolicy(request, options, callback); + } + + /** + * Returns permissions that a caller has on the specified resource. If the + * resource does not exist, this will return an empty set of + * permissions, not a NOT_FOUND error. + * + * Note: This operation is designed to be used for building + * permission-aware UIs and command-line tools, not for authorization + * checking. This operation may "fail open" without warning. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.resource + * REQUIRED: The resource for which the policy detail is being requested. + * See the operation documentation for the appropriate value for this field. + * @param {string[]} request.permissions + * The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see {@link https://cloud.google.com/iam/docs/overview#permissions | IAM Overview }. + * @param {Object} [options] + * Optional parameters. You can override the default settings for this call, e.g, timeout, + * retries, paginations, etc. See {@link https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html | gax.CallOptions} for the details. + * @param {function(?Error, ?Object)} [callback] + * The function which will be called with the result of the API call. + * + * The second parameter to the callback is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.iam.v1.TestIamPermissionsResponse | TestIamPermissionsResponse}. + * The promise has a method named "cancel" which cancels the ongoing API call. + * + */ + testIamPermissions( + request: IamProtos.google.iam.v1.TestIamPermissionsRequest, + options?: + | gax.CallOptions + | Callback< + IamProtos.google.iam.v1.TestIamPermissionsResponse, + IamProtos.google.iam.v1.TestIamPermissionsRequest | null | undefined, + {} | null | undefined + >, + callback?: Callback< + IamProtos.google.iam.v1.TestIamPermissionsResponse, + IamProtos.google.iam.v1.TestIamPermissionsRequest | null | undefined, + {} | null | undefined + > + ): Promise<[IamProtos.google.iam.v1.TestIamPermissionsResponse]> { + return this.iamClient.testIamPermissions(request, options, callback); + } + + /** + * Gets information about a location. + * + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * Resource name for the location. + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html | CallOptions} for more details. + * @returns {Promise} - The promise which resolves to an array. + * The first element of the array is an object representing {@link google.cloud.location.Location | Location}. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#regular-methods | documentation } + * for more details and examples. + * @example + * ``` + * const [response] = await client.getLocation(request); + * ``` + */ + getLocation( + request: LocationProtos.google.cloud.location.IGetLocationRequest, + options?: + | gax.CallOptions + | Callback< + LocationProtos.google.cloud.location.ILocation, + | LocationProtos.google.cloud.location.IGetLocationRequest + | null + | undefined, + {} | null | undefined + >, + callback?: Callback< + LocationProtos.google.cloud.location.ILocation, + | LocationProtos.google.cloud.location.IGetLocationRequest + | null + | undefined, + {} | null | undefined + > + ): Promise { + return this.locationsClient.getLocation(request, options, callback); + } + + /** + * Lists information about the supported locations for this service. Returns an iterable object. + * + * `for`-`await`-`of` syntax is used with the iterable to get response elements on-demand. + * @param {Object} request + * The request object that will be sent. + * @param {string} request.name + * The resource that owns the locations collection, if applicable. + * @param {string} request.filter + * The standard list filter. + * @param {number} request.pageSize + * The standard list page size. + * @param {string} request.pageToken + * The standard list page token. + * @param {object} [options] + * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. + * @returns {Object} + * An iterable Object that allows {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols | async iteration }. + * When you iterate the returned iterable, each element will be an object representing + * {@link google.cloud.location.Location | Location}. The API will be called under the hood as needed, once per the page, + * so you can stop the iteration when you don't need more results. + * Please see the {@link https://github.com/googleapis/gax-nodejs/blob/master/client-libraries.md#auto-pagination | documentation } + * for more details and examples. + * @example + * ``` + * const iterable = client.listLocationsAsync(request); + * for await (const response of iterable) { + * // process response + * } + * ``` + */ + listLocationsAsync( + request: LocationProtos.google.cloud.location.IListLocationsRequest, + options?: CallOptions + ): AsyncIterable { + return this.locationsClient.listLocationsAsync(request, options); + } + + // -------------------- + // -- Path templates -- + // -------------------- + + /** + * Return a fully-qualified sequence resource name string. + * + * @param {string} sequence + * @returns {string} Resource name string. + */ + sequencePath(sequence: string) { + return this.pathTemplates.sequencePathTemplate.render({ + sequence: sequence, + }); + } + + /** + * Parse the sequence from Sequence resource. + * + * @param {string} sequenceName + * A fully-qualified path representing Sequence resource. + * @returns {string} A string representing the sequence. + */ + matchSequenceFromSequenceName(sequenceName: string) { + return this.pathTemplates.sequencePathTemplate.match(sequenceName).sequence; + } + + /** + * Return a fully-qualified sequenceReport resource name string. + * + * @param {string} sequence + * @returns {string} Resource name string. + */ + sequenceReportPath(sequence: string) { + return this.pathTemplates.sequenceReportPathTemplate.render({ + sequence: sequence, + }); + } + + /** + * Parse the sequence from SequenceReport resource. + * + * @param {string} sequenceReportName + * A fully-qualified path representing SequenceReport resource. + * @returns {string} A string representing the sequence. + */ + matchSequenceFromSequenceReportName(sequenceReportName: string) { + return this.pathTemplates.sequenceReportPathTemplate.match( + sequenceReportName + ).sequence; + } + + /** + * Return a fully-qualified streamingSequence resource name string. + * + * @param {string} streaming_sequence + * @returns {string} Resource name string. + */ + streamingSequencePath(streamingSequence: string) { + return this.pathTemplates.streamingSequencePathTemplate.render({ + streaming_sequence: streamingSequence, + }); + } + + /** + * Parse the streaming_sequence from StreamingSequence resource. + * + * @param {string} streamingSequenceName + * A fully-qualified path representing StreamingSequence resource. + * @returns {string} A string representing the streaming_sequence. + */ + matchStreamingSequenceFromStreamingSequenceName( + streamingSequenceName: string + ) { + return this.pathTemplates.streamingSequencePathTemplate.match( + streamingSequenceName + ).streaming_sequence; + } + + /** + * Return a fully-qualified streamingSequenceReport resource name string. + * + * @param {string} streaming_sequence + * @returns {string} Resource name string. + */ + streamingSequenceReportPath(streamingSequence: string) { + return this.pathTemplates.streamingSequenceReportPathTemplate.render({ + streaming_sequence: streamingSequence, + }); + } + + /** + * Parse the streaming_sequence from StreamingSequenceReport resource. + * + * @param {string} streamingSequenceReportName + * A fully-qualified path representing StreamingSequenceReport resource. + * @returns {string} A string representing the streaming_sequence. + */ + matchStreamingSequenceFromStreamingSequenceReportName( + streamingSequenceReportName: string + ) { + return this.pathTemplates.streamingSequenceReportPathTemplate.match( + streamingSequenceReportName + ).streaming_sequence; + } + + /** + * Terminate the gRPC channel and close the client. + * + * The client will no longer be usable and all future behavior is undefined. + * @returns {Promise} A promise that resolves when the client is closed. + */ + close(): Promise { + if (this.sequenceServiceStub && !this._terminated) { + return this.sequenceServiceStub.then(stub => { + this._terminated = true; + stub.close(); + this.iamClient.close(); + this.locationsClient.close(); + }); + } + return Promise.resolve(); + } +} diff --git a/gax/test/showcase-echo-client/src/v1beta1/sequence_service_client_config.json b/gax/test/showcase-echo-client/src/v1beta1/sequence_service_client_config.json new file mode 100644 index 000000000..955c6fcec --- /dev/null +++ b/gax/test/showcase-echo-client/src/v1beta1/sequence_service_client_config.json @@ -0,0 +1,50 @@ +{ + "interfaces": { + "google.showcase.v1beta1.SequenceService": { + "retry_codes": { + "non_idempotent": [], + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ] + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "CreateSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "CreateStreamingSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetSequenceReport": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetStreamingSequenceReport": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "AttemptSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "AttemptStreamingSequence": { + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/gax/test/showcase-echo-client/src/v1beta1/sequence_service_proto_list.json b/gax/test/showcase-echo-client/src/v1beta1/sequence_service_proto_list.json new file mode 100644 index 000000000..76b3da61a --- /dev/null +++ b/gax/test/showcase-echo-client/src/v1beta1/sequence_service_proto_list.json @@ -0,0 +1,4 @@ +[ + "../../protos/google/showcase/v1beta1/echo.proto", + "../../protos/google/showcase/v1beta1/sequence.proto" +] diff --git a/gax/test/showcase-server/src/index.ts b/gax/test/showcase-server/src/index.ts index 943c283e0..bf1ff6ab7 100644 --- a/gax/test/showcase-server/src/index.ts +++ b/gax/test/showcase-server/src/index.ts @@ -36,7 +36,7 @@ export class ShowcaseServer { const testDir = path.join(process.cwd(), '.showcase-server-dir'); const platform = process.platform; const arch = process.arch === 'x64' ? 'amd64' : process.arch; - const showcaseVersion = process.env['SHOWCASE_VERSION'] || '0.23.0'; + const showcaseVersion = process.env['SHOWCASE_VERSION'] || '0.28.4'; const tarballFilename = `gapic-showcase-${showcaseVersion}-${platform}-${arch}.tar.gz`; const fallbackServerUrl = `https://github.com/googleapis/gapic-showcase/releases/download/v${showcaseVersion}/${tarballFilename}`; const binaryName = './gapic-showcase'; diff --git a/gax/test/system-test/test.clientlibs.ts b/gax/test/system-test/test.clientlibs.ts index 3733903ca..fcc14e761 100644 --- a/gax/test/system-test/test.clientlibs.ts +++ b/gax/test/system-test/test.clientlibs.ts @@ -165,14 +165,6 @@ async function runSystemTest( return await runScript(packageName, inMonorepo, 'system-test'); } -// nodejs-kms does not have system test. -async function runSamplesTest( - packageName: string, - inMonorepo: boolean -): Promise { - return await runScript(packageName, inMonorepo, 'samples-test'); -} - describe('Run system tests for some libraries', () => { before(async () => { console.log('Packing google-gax...'); @@ -218,21 +210,4 @@ describe('Run system tests for some libraries', () => { } }); }); - - // KMS api has IAM service injected from gax. All its IAM related test are in samples-test. - // KMS is in the google-cloud-node monorepo - // Temporarily skipped to avoid circular dependency issue. - /*describe('kms', () => { - before(async () => { - await preparePackage('kms', true); - }); - it('should pass samples tests', async function () { - const result = await runSamplesTest('kms', true); - if (result === TestResult.SKIP) { - this.skip(); - } else if (result === TestResult.FAIL) { - throw new Error('Test failed'); - } - }); - });*/ }); diff --git a/gax/test/test-application/package.json b/gax/test/test-application/package.json index c2ad41876..3fc47060c 100644 --- a/gax/test/test-application/package.json +++ b/gax/test/test-application/package.json @@ -20,8 +20,8 @@ "start": "node build/src/index.js" }, "devDependencies": { - "mocha": "^9.1.4", - "typescript": "^4.5.5" + "mocha": "^10.0.1", + "typescript": "^5.1.6" }, "dependencies": { "@grpc/grpc-js": "~1.6.0", diff --git a/gax/test/test-application/src/index.ts b/gax/test/test-application/src/index.ts index 7642dc242..8d13f8618 100644 --- a/gax/test/test-application/src/index.ts +++ b/gax/test/test-application/src/index.ts @@ -15,13 +15,22 @@ */ 'use strict'; -import {EchoClient} from 'showcase-echo-client'; +import {EchoClient, SequenceServiceClient, protos} from 'showcase-echo-client'; import {ShowcaseServer} from 'showcase-server'; import * as assert from 'assert'; import {promises as fsp} from 'fs'; import * as path from 'path'; -import {protobuf, grpc, GoogleError, GoogleAuth} from 'google-gax'; +import { + protobuf, + grpc, + GoogleError, + GoogleAuth, + Status, + createBackoffSettings, + RetryOptions, +} from 'google-gax'; +import {RequestType} from 'google-gax/build/src/apitypes'; async function testShowcase() { const grpcClientOpts = { @@ -29,6 +38,12 @@ async function testShowcase() { sslCreds: grpc.credentials.createInsecure(), }; + const grpcClientOptsWithServerStreamingRetries = { + grpc, + sslCreds: grpc.credentials.createInsecure(), + gaxServerStreamingRetries: true, + }; + const fakeGoogleAuth = { getClient: async () => { return { @@ -56,6 +71,12 @@ async function testShowcase() { }; const grpcClient = new EchoClient(grpcClientOpts); + const grpcClientWithServerStreamingRetries = new EchoClient( + grpcClientOptsWithServerStreamingRetries + ); + const grpcSequenceClientWithServerStreamingRetries = + new SequenceServiceClient(grpcClientOptsWithServerStreamingRetries); + const restClient = new EchoClient(restClientOpts); const restClientCompat = new EchoClient(restClientOptsCompat); @@ -84,6 +105,85 @@ async function testShowcase() { await testCollectThrows(restClientCompat); // REGAPIC does not support client streaming await testChatThrows(restClientCompat); // REGAPIC does not support bidi streaming await testWait(restClientCompat); + // Testing with gaxServerStreamingRetries being true + await testServerStreamingRetryOptions( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingRetriesWithShouldRetryFn( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingRetrieswithRetryOptions( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingRetrieswithRetryRequestOptions( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingRetrieswithRetryRequestOptionsResumptionStrategy( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingRetrieswithRetryRequestOptionsErrorsOnBadResumptionStrategy( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingThrowsClassifiedTransientErrorNote( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingRetriesAndThrowsClassifiedTransientErrorNote( + grpcSequenceClientWithServerStreamingRetries + ); + + await testServerStreamingThrowsCannotSetTotalTimeoutMillisMaxRetries( + grpcSequenceClientWithServerStreamingRetries + ); + + await testEcho(grpcClientWithServerStreamingRetries); + await testEchoError(grpcClientWithServerStreamingRetries); + await testExpand(grpcClientWithServerStreamingRetries); + await testPagedExpand(grpcClientWithServerStreamingRetries); + await testPagedExpandAsync(grpcClientWithServerStreamingRetries); + await testCollect(grpcClientWithServerStreamingRetries); + await testChat(grpcClientWithServerStreamingRetries); + await testWait(grpcClientWithServerStreamingRetries); +} + +function createStreamingSequenceRequestFactory( + statusCodeList: Status[], + delayList: number[], + failIndexs: number[], + content: string +) { + const request = + new protos.google.showcase.v1beta1.CreateStreamingSequenceRequest(); + const streamingSequence = + new protos.google.showcase.v1beta1.StreamingSequence(); + + for (let i = 0; i < statusCodeList.length; i++) { + const delay = new protos.google.protobuf.Duration(); + delay.seconds = delayList[i]; + + const status = new protos.google.rpc.Status(); + status.code = statusCodeList[i]; + status.message = statusCodeList[i].toString(); + + const response = + new protos.google.showcase.v1beta1.StreamingSequence.Response(); + response.delay = delay; + response.status = status; + response.responseIndex = failIndexs[i]; + + streamingSequence.responses.push(response); + streamingSequence.content = content; + } + + request.streamingSequence = streamingSequence; + + return request; } async function testEcho(client: EchoClient) { @@ -367,6 +467,557 @@ async function testWait(client: EchoClient) { assert.deepStrictEqual(response.content, request.success.content); } +async function testServerStreamingRetryOptions(client: SequenceServiceClient) { + const finalData: string[] = []; + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + + const retryOptions = new RetryOptions([], backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.OK], + [0.1], + [11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise(resolve => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + //Do Nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetrieswithRetryOptions( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + + const retryOptions = new RetryOptions([14, 4], backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise(resolve => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + //Do Nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This This is This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetriesWithShouldRetryFn( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const shouldRetryFn = function checkRetry(error: GoogleError) { + return [14, 4].includes(error!.code!); + }; + + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + + const retryOptions = new RetryOptions([], backoffSettings, shouldRetryFn); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise(resolve => { + const sequence = response[0]; + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + // Do nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This This is This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetrieswithRetryRequestOptions( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const retryRequestOptions = { + objectMode: true, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function checkRetry(error: GoogleError) { + return [14, 4].includes(error!.code!); + }, + }; + + const settings = { + retryRequestOptions: retryRequestOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise(resolve => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + // Do Nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.equal( + finalData.join(' '), + 'This This is This is testing the brand new and shiny StreamingSequence server 3' + ); + }); +} +async function testServerStreamingRetrieswithRetryRequestOptionsResumptionStrategy( + client: SequenceServiceClient +) { + const finalData: string[] = []; + const shouldRetryFn = (error: GoogleError) => { + return [4, 14].includes(error!.code!); + }; + const backoffSettings = createBackoffSettings( + 10000, + 2.5, + 1000, + null, + 1.5, + 3000, + 600000 + ); + const getResumptionRequestFn = (request: RequestType) => { + const newRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest() as unknown as RequestType; + newRequest.name = request.name; + newRequest.lastFailIndex = 5; + return newRequest as unknown as RequestType; + }; + + const retryOptions = new RetryOptions( + [], + backoffSettings, + shouldRetryFn, + getResumptionRequestFn + ); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + const response = await client.createStreamingSequence(request); + await new Promise(resolve => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('data', (response: {content: string}) => { + finalData.push(response.content); + }); + attemptStream.on('error', () => { + // do nothing + }); + attemptStream.on('end', () => { + attemptStream.end(); + resolve(); + }); + }).then(() => { + assert.deepStrictEqual( + finalData.join(' '), + 'This new and new and shiny StreamingSequence server 3' + ); + }); +} + +async function testServerStreamingRetrieswithRetryRequestOptionsErrorsOnBadResumptionStrategy( + client: SequenceServiceClient +) { + const shouldRetryFn = (error: GoogleError) => { + return [4, 14].includes(error!.code!); + }; + const backoffSettings = createBackoffSettings( + 10000, + 2.5, + 1000, + null, + 1.5, + 3000, + 600000 + ); + const getResumptionRequestFn = () => { + // return a bad resumption strategy + return {}; + }; + + const retryOptions = new RetryOptions( + [], + backoffSettings, + shouldRetryFn, + getResumptionRequestFn + ); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + const allowedCodes = [4, 14]; + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + + attemptStream.on('error', (e: GoogleError) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 3); + assert.match(err.note!, /not classified as transient/); + } + ); +} + +async function testServerStreamingThrowsClassifiedTransientErrorNote( + client: SequenceServiceClient +) { + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + const allowedCodes = [4]; + const retryOptions = new RetryOptions(allowedCodes, backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('error', (e: GoogleError) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 14); + assert.match(err.note!, /not classified as transient/); + } + ); +} + +async function testServerStreamingRetriesAndThrowsClassifiedTransientErrorNote( + client: SequenceServiceClient +) { + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + const allowedCodes = [14]; + const retryOptions = new RetryOptions(allowedCodes, backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.DEADLINE_EXCEEDED, Status.OK], + [0.1, 0.1, 0.1], + [1, 2, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('error', (e: GoogleError) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 4); + assert.match(err.note!, /not classified as transient/); + } + ); +} + +async function testServerStreamingThrowsCannotSetTotalTimeoutMillisMaxRetries( + client: SequenceServiceClient +) { + const backoffSettings = createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 10000 + ); + const allowedCodes = [14]; + backoffSettings.maxRetries = 1; + const retryOptions = new RetryOptions(allowedCodes, backoffSettings); + + const settings = { + retry: retryOptions, + }; + + client.initialize(); + + const request = createStreamingSequenceRequestFactory( + [Status.UNAVAILABLE, Status.OK], + [0.1, 0.1], + [1, 11], + 'This is testing the brand new and shiny StreamingSequence server 3' + ); + + const response = await client.createStreamingSequence(request); + await new Promise((_, reject) => { + const sequence = response[0]; + + const attemptRequest = + new protos.google.showcase.v1beta1.AttemptStreamingSequenceRequest(); + attemptRequest.name = sequence.name!; + + const attemptStream = client.attemptStreamingSequence( + attemptRequest, + settings + ); + attemptStream.on('error', (e: GoogleError) => { + if (!allowedCodes.includes(e.code!)) { + reject(e); + } + }); + }).then( + () => { + assert(false); + }, + (err: GoogleError) => { + assert.strictEqual(err.code, 3); + assert.match( + err.message, + /Cannot set both totalTimeoutMillis and maxRetries/ + ); + } + ); +} + async function main() { const showcaseServer = new ShowcaseServer(); try { diff --git a/gax/test/unit/apiCallable.ts b/gax/test/unit/apiCallable.ts index 71f2904a4..ae98bbf41 100644 --- a/gax/test/unit/apiCallable.ts +++ b/gax/test/unit/apiCallable.ts @@ -19,6 +19,7 @@ import {status} from '@grpc/grpc-js'; import {afterEach, describe, it} from 'mocha'; import * as sinon from 'sinon'; +import {RequestType} from '../../src/apitypes'; import * as gax from '../../src/gax'; import {GoogleError} from '../../src/googleError'; import * as utils from './utils'; @@ -66,8 +67,8 @@ describe('createApiCall', () => { const now = new Date(); const originalDeadline = now.getTime() + 100; const expectedDeadline = now.getTime() + 200; - assert((resp as any)! > originalDeadline); - assert((resp as any)! <= expectedDeadline); + assert((resp as unknown as number)! > originalDeadline); + assert((resp as unknown as number)! <= expectedDeadline); done(); }); }); @@ -139,20 +140,23 @@ describe('createApiCall', () => { done(); }); }); - - it('override just custom retry.retrycodes', done => { + it('override just custom retry.retryCodes with retry codes', done => { const initialRetryCodes = [1]; const overrideRetryCodes = [1, 2, 3]; // eslint-disable-next-line @typescript-eslint/no-explicit-any sinon.stub(retries, 'retryable').callsFake((func, retry): any => { - assert.strictEqual(retry.retryCodes, overrideRetryCodes); + try { + assert.strictEqual(retry.retryCodes, overrideRetryCodes); + return func; + } catch (err) { + done(err); + } return func; }); function func() { done(); } - const apiCall = createApiCall(func, { settings: { retry: gax.createRetryOptions(initialRetryCodes, { @@ -175,6 +179,46 @@ describe('createApiCall', () => { } ); }); + it('errors when you override custom retry.shouldRetryFn with a function on a non streaming call', async () => { + function neverRetry() { + return false; + } + const initialRetryCodes = [1]; + const overrideRetryCodes = neverRetry; + + function func() { + return Promise.resolve(); + } + + const apiCall = createApiCall(func, { + settings: { + retry: gax.createRetryOptions(initialRetryCodes, { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + }, + }); + try { + await apiCall( + {}, + { + retry: { + shouldRetryFn: overrideRetryCodes, + }, + } + ); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.message, + 'Using a function to determine retry eligibility is only supported with server streaming calls' + ); + } + }); it('override just custom retry.backoffSettings', done => { const initialBackoffSettings = gax.createDefaultBackoffSettings(); @@ -212,6 +256,54 @@ describe('createApiCall', () => { } ); }); + + it('errors when a resumption strategy is passed for a non streaming call', async () => { + const initialBackoffSettings = gax.createDefaultBackoffSettings(); + const overriBackoffSettings = gax.createBackoffSettings( + 100, + 1.2, + 1000, + null, + 1.5, + 3000, + 4500 + ); + // "resumption" strategy is to just return the original request + const getResumptionRequestFn = (originalRequest: RequestType) => { + return originalRequest; + }; + + function func() { + Promise.resolve(); + } + const apiCall = createApiCall(func, { + settings: { + retry: gax.createRetryOptions( + [1], + initialBackoffSettings, + undefined, + getResumptionRequestFn + ), + }, + }); + + try { + await apiCall( + {}, + { + retry: { + backoffSettings: overriBackoffSettings, + }, + } + ); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.message, + 'Resumption strategy can only be used with server streaming retries' + ); + } + }); }); describe('Promise', () => { @@ -500,7 +592,8 @@ describe('retryable', () => { }); }); - // maxRetries is unsupported, and intended for internal use only. + // maxRetries is unsupported, and intended for internal use only or + // use with retry-request backwards compatibility it('errors when totalTimeoutMillis and maxRetries set', done => { const maxRetries = 5; const backoff = gax.createMaxRetriesBackoffSettings( diff --git a/gax/test/unit/gax.ts b/gax/test/unit/gax.ts index d7eb4a99d..846dd51a0 100644 --- a/gax/test/unit/gax.ts +++ b/gax/test/unit/gax.ts @@ -78,7 +78,7 @@ function expectRetryOptions(obj: gax.RetryOptions) { // eslint-disable-next-line no-prototype-builtins assert.ok(obj.hasOwnProperty(k)) ); - assert.ok(obj.retryCodes instanceof Array); + assert.ok(Array.isArray(obj.retryCodes)); expectBackoffSettings(obj.backoffSettings); } diff --git a/gax/test/unit/streaming.ts b/gax/test/unit/streaming.ts index 5feab8dee..9aa9c5ffc 100644 --- a/gax/test/unit/streaming.ts +++ b/gax/test/unit/streaming.ts @@ -21,12 +21,14 @@ import * as sinon from 'sinon'; import {afterEach, describe, it} from 'mocha'; import {PassThrough} from 'stream'; -import {GaxCallStream, GRPCCall} from '../../src/apitypes'; +import {GaxCallStream, GRPCCall, RequestType} from '../../src/apitypes'; import {createApiCall} from '../../src/createApiCall'; +import {StreamingApiCaller} from '../../src/streamingCalls/streamingApiCaller'; import * as gax from '../../src/gax'; import {StreamDescriptor} from '../../src/streamingCalls/streamDescriptor'; import * as streaming from '../../src/streamingCalls/streaming'; import {APICallback} from '../../src/apitypes'; +import * as warnings from '../../src/warnings'; import internal = require('stream'); import {StreamArrayParser} from '../../src/streamArrayParser'; import path = require('path'); @@ -39,14 +41,15 @@ function createApiCallStreaming( | Promise | sinon.SinonSpy, internal.Transform | StreamArrayParser>, type: streaming.StreamType, - rest?: boolean + rest?: boolean, + gaxStreamingRetries?: boolean ) { const settings = new gax.CallSettings(); return createApiCall( //@ts-ignore Promise.resolve(func), settings, - new StreamDescriptor(type, rest) + new StreamDescriptor(type, rest, gaxStreamingRetries) ) as GaxCallStream; } @@ -159,41 +162,6 @@ describe('streaming', () => { s.end(); }); - it('allows custome CallOptions.retry settings', done => { - sinon - .stub(streaming.StreamProxy.prototype, 'forwardEvents') - .callsFake(stream => { - assert(stream instanceof internal.Stream); - done(); - }); - const spy = sinon.spy((...args: Array<{}>) => { - assert.strictEqual(args.length, 3); - const s = new PassThrough({ - objectMode: true, - }); - return s; - }); - - const apiCall = createApiCallStreaming( - spy, - streaming.StreamType.SERVER_STREAMING - ); - - apiCall( - {}, - { - retry: gax.createRetryOptions([1], { - initialRetryDelayMillis: 100, - retryDelayMultiplier: 1.2, - maxRetryDelayMillis: 1000, - rpcTimeoutMultiplier: 1.5, - maxRpcTimeoutMillis: 3000, - totalTimeoutMillis: 4500, - }), - } - ); - }); - it('forwards metadata and status', done => { const responseMetadata = {metadata: true}; const status = {code: 0, metadata: responseMetadata}; @@ -266,6 +234,8 @@ describe('streaming', () => { }); it('cancels in the middle', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any function schedulePush(s: any, c: number) { const intervalId = setInterval(() => { @@ -297,7 +267,19 @@ describe('streaming', () => { func, streaming.StreamType.SERVER_STREAMING ); - const s = apiCall({}, undefined); + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 0, + }), + } + ); let counter = 0; const expectedCount = 5; s.on('data', data => { @@ -313,6 +295,14 @@ describe('streaming', () => { assert.strictEqual(err, cancelError); done(); }); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ) + ); }); it('emit response when stream received metadata event', done => { @@ -392,6 +382,85 @@ describe('streaming', () => { assert.strictEqual(responseCallback.callCount, 1); }); }); + it('emit response when stream received metadata event and new gax retries is enabled', done => { + const responseMetadata = {metadata: true}; + const expectedStatus = {code: 0, metadata: responseMetadata}; + const expectedResponse = { + code: 200, + message: 'OK', + details: '', + metadata: responseMetadata, + }; + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push(null); + setImmediate(() => { + s.emit('metadata', responseMetadata); + }); + s.on('end', () => { + setTimeout(() => { + s.emit('status', expectedStatus); + }, 10); + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // new gax retries + ); + const s = apiCall({}, undefined); + let receivedMetadata: {}; + let receivedStatus: {}; + let receivedResponse: {}; + let ended = false; + + function check() { + if ( + typeof receivedMetadata !== 'undefined' && + typeof receivedStatus !== 'undefined' && + typeof receivedResponse !== 'undefined' && + ended + ) { + assert.deepStrictEqual(receivedMetadata, responseMetadata); + assert.deepStrictEqual(receivedStatus, expectedStatus); + assert.deepStrictEqual(receivedResponse, expectedResponse); + done(); + } + } + + const dataCallback = sinon.spy(data => { + assert.deepStrictEqual(data, undefined); + }); + const responseCallback = sinon.spy(); + assert.strictEqual(s.readable, true); + assert.strictEqual(s.writable, false); + s.on('data', dataCallback); + s.on('metadata', data => { + receivedMetadata = data; + check(); + }); + s.on('response', data => { + receivedResponse = data; + responseCallback(); + check(); + }); + s.on('status', data => { + receivedStatus = data; + check(); + }); + s.on('end', () => { + ended = true; + check(); + assert.strictEqual(dataCallback.callCount, 0); + assert.strictEqual(responseCallback.callCount, 1); + }); + }); it('emit response when stream received no metadata event', done => { const responseMetadata = {metadata: true}; @@ -409,7 +478,6 @@ describe('streaming', () => { s.push(null); s.on('end', () => { setTimeout(() => { - console.log('emit status event'); s.emit('status', expectedStatus); }, 10); }); @@ -460,8 +528,78 @@ describe('streaming', () => { assert.strictEqual(responseCallback.callCount, 1); }); }); + it('emit response when stream received no metadata event with new gax retries', done => { + const responseMetadata = {metadata: true}; + const expectedStatus = {code: 0, metadata: responseMetadata}; + const expectedResponse = { + code: 200, + message: 'OK', + details: '', + }; + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push(null); + s.on('end', () => { + setTimeout(() => { + s.emit('status', expectedStatus); + }, 10); + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // new gax retries + ); + const s = apiCall({}, undefined); + let receivedStatus: {}; + let receivedResponse: {}; + let ended = false; + + function check() { + if ( + typeof receivedStatus !== 'undefined' && + typeof receivedResponse !== 'undefined' && + ended + ) { + assert.deepStrictEqual(receivedStatus, expectedStatus); + assert.deepStrictEqual(receivedResponse, expectedResponse); + done(); + } + } + + const dataCallback = sinon.spy(data => { + assert.deepStrictEqual(data, undefined); + }); + const responseCallback = sinon.spy(); + assert.strictEqual(s.readable, true); + assert.strictEqual(s.writable, false); + s.on('data', dataCallback); + s.on('response', data => { + receivedResponse = data; + responseCallback(); + check(); + }); + s.on('status', data => { + receivedStatus = data; + check(); + }); + s.on('end', () => { + ended = true; + check(); + assert.strictEqual(dataCallback.callCount, 0); + assert.strictEqual(responseCallback.callCount, 1); + }); + }); it('emit parsed GoogleError', done => { + const warnStub = sinon.stub(warnings, 'warn'); + const errorInfoObj = { reason: 'SERVICE_DISABLED', domain: 'googleapis.com', @@ -488,6 +626,7 @@ describe('streaming', () => { details: 'Failed to read', metadata: metadata, }); + const spy = sinon.spy((...args: Array<{}>) => { assert.strictEqual(args.length, 3); const s = new PassThrough({ @@ -497,14 +636,33 @@ describe('streaming', () => { setImmediate(() => { s.emit('error', error); }); + setImmediate(() => { + s.emit('end'); + }); return s; }); const apiCall = createApiCallStreaming( spy, streaming.StreamType.SERVER_STREAMING ); - const s = apiCall({}, undefined); + + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 0, + }), + } + ); + s.on('error', err => { + s.pause(); + s.destroy(); assert(err instanceof GoogleError); assert.deepStrictEqual(err.message, 'test error'); assert.strictEqual(err.domain, errorInfoObj.domain); @@ -515,52 +673,1501 @@ describe('streaming', () => { ); done(); }); + s.on('end', () => { + done(); + }); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ) + ); }); -}); + it('emit parsed GoogleError when new retries are enabled', done => { + const errorInfoObj = { + reason: 'SERVICE_DISABLED', + domain: 'googleapis.com', + metadata: { + consumer: 'projects/455411330361', + service: 'translate.googleapis.com', + }, + }; + const errorProtoJson = require('../../protos/status.json'); + const root = protobuf.Root.fromJSON(errorProtoJson); + const errorInfoType = root.lookupType('ErrorInfo'); + const buffer = errorInfoType.encode(errorInfoObj).finish() as Buffer; + const any = { + type_url: 'type.googleapis.com/google.rpc.ErrorInfo', + value: buffer, + }; + const status = {code: 3, message: 'test', details: [any]}; + const Status = root.lookupType('google.rpc.Status'); + const status_buffer = Status.encode(status).finish() as Buffer; + const metadata = new Metadata(); + metadata.set('grpc-status-details-bin', status_buffer); + const error = Object.assign(new GoogleError('test error'), { + code: 5, + details: 'Failed to read', + metadata: metadata, + }); -describe('REST streaming apiCall return StreamArrayParser', () => { - const protos_path = path.resolve(__dirname, '..', 'fixtures', 'user.proto'); - const root = protobuf.loadSync(protos_path); - const UserService = root.lookupService('UserService'); - UserService.resolveAll(); - const streamMethod = UserService.methods['RunQuery']; - it('forwards data, end event', done => { const spy = sinon.spy((...args: Array<{}>) => { assert.strictEqual(args.length, 3); - const s = new StreamArrayParser(streamMethod); - s.push({resources: [1, 2]}); - s.push({resources: [3, 4, 5]}); + const s = new PassThrough({ + objectMode: true, + }); s.push(null); + setImmediate(() => { + s.emit('error', error); + }); + setImmediate(() => { + s.emit('end'); + }); return s; }); const apiCall = createApiCallStreaming( spy, streaming.StreamType.SERVER_STREAMING, - true + false, + true // new retry behavior enabled + ); + + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 0, + }), + } + ); + + s.on('error', err => { + s.pause(); + s.destroy(); + assert(err instanceof GoogleError); + assert.deepStrictEqual(err.message, 'test error'); + assert.strictEqual(err.domain, errorInfoObj.domain); + assert.strictEqual(err.reason, errorInfoObj.reason); + assert.strictEqual( + JSON.stringify(err.errorInfoMetadata), + JSON.stringify(errorInfoObj.metadata) + ); + done(); + }); + s.on('end', () => { + done(); + }); + }); + it('emit transient error message on first error when new retries are enabled', done => { + const errorInfoObj = { + reason: 'SERVICE_DISABLED', + domain: 'googleapis.com', + metadata: { + consumer: 'projects/455411330361', + service: 'translate.googleapis.com', + }, + }; + const errorProtoJson = require('../../protos/status.json'); + const root = protobuf.Root.fromJSON(errorProtoJson); + const errorInfoType = root.lookupType('ErrorInfo'); + const buffer = errorInfoType.encode(errorInfoObj).finish() as Buffer; + const any = { + type_url: 'type.googleapis.com/google.rpc.ErrorInfo', + value: buffer, + }; + const status = {code: 3, message: 'test', details: [any]}; + const Status = root.lookupType('google.rpc.Status'); + const status_buffer = Status.encode(status).finish() as Buffer; + const metadata = new Metadata(); + metadata.set('grpc-status-details-bin', status_buffer); + const error = Object.assign(new GoogleError('test error'), { + code: 3, + details: 'Failed to read', + metadata: metadata, + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push(null); + setImmediate(() => { + // emits an error not in our included retry codes + s.emit('error', error); + }); + setImmediate(() => { + s.emit('status', status); + }); + + return s; + }); + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // new retry behavior enabled + ); + + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 1, // max retries or timeout must be > 0 in order to reach the code we want to test + }), + } + ); + + s.on('error', err => { + s.pause(); + s.destroy(); + + assert(err instanceof GoogleError); + assert.deepStrictEqual(err.message, 'test error'); + assert.deepStrictEqual( + err.note, + 'Exception occurred in retry method that was not classified as transient' + ); + assert.strictEqual(err.domain, errorInfoObj.domain); + assert.strictEqual(err.reason, errorInfoObj.reason); + assert.strictEqual( + JSON.stringify(err.errorInfoMetadata), + JSON.stringify(errorInfoObj.metadata) + ); + done(); + }); + }); + it('emit transient error on second or later error when new retries are enabled', done => { + // stubbing cancel is needed because PassThrough doesn't have + // a cancel method and cancel is called as part of the retry + const cancelStub = sinon.stub(streaming.StreamProxy.prototype, 'cancel'); + + const errorInfoObj = { + reason: 'SERVICE_DISABLED', + domain: 'googleapis.com', + metadata: { + consumer: 'projects/455411330361', + service: 'translate.googleapis.com', + }, + }; + const errorProtoJson = require('../../protos/status.json'); + const root = protobuf.Root.fromJSON(errorProtoJson); + const errorInfoType = root.lookupType('ErrorInfo'); + const buffer = errorInfoType.encode(errorInfoObj).finish() as Buffer; + const any = { + type_url: 'type.googleapis.com/google.rpc.ErrorInfo', + value: buffer, + }; + const status = {code: 3, message: 'test', details: [any]}; + const Status = root.lookupType('google.rpc.Status'); + const status_buffer = Status.encode(status).finish() as Buffer; + const metadata = new Metadata(); + metadata.set('grpc-status-details-bin', status_buffer); + const error = Object.assign(new GoogleError('test error'), { + code: 2, + details: 'Failed to read', + metadata: metadata, + }); + const error2 = Object.assign(new GoogleError('test error 2'), { + code: 3, + details: 'Failed to read', + metadata: metadata, + }); + let count = 0; + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + let e = new Error(); + switch (count) { + case 0: + e = error; + + s.push(null); + setImmediate(() => { + s.emit('error', e); // is included in our retry codes + }); + setImmediate(() => { + s.emit('status', status); + }); + count++; + return s; + + case 1: + e = error2; // is not in our retry codes + + s.push(null); + setImmediate(() => { + s.emit('error', e); + }); + + setImmediate(() => { + s.emit('status', status); + }); + count++; + + return s; + default: + setImmediate(() => { + s.emit('end'); + }); + return s; + } + }); + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // new retry behavior enabled + ); + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([2, 5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 2, // max retries or timeout must be > 0 in order to reach the code we want to test + }), + } + ); + + s.on('error', err => { + s.pause(); + s.destroy(); + assert(err instanceof GoogleError); + assert.deepStrictEqual(err.message, 'test error 2'); + assert.deepStrictEqual( + err.note, + 'Exception occurred in retry method that was not classified as transient' + ); + assert.strictEqual(err.domain, errorInfoObj.domain); + assert.strictEqual(err.reason, errorInfoObj.reason); + assert.strictEqual( + JSON.stringify(err.errorInfoMetadata), + JSON.stringify(errorInfoObj.metadata) + ); + assert.strictEqual(cancelStub.callCount, 1); + done(); + }); + }); + + it('emit error and retry once', done => { + const firstError = Object.assign(new GoogleError('UNAVAILABLE'), { + code: 14, + details: 'UNAVAILABLE', + }); + let counter = 0; + const expectedStatus = {code: 0}; + const receivedData: string[] = []; + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + setImmediate(() => { + s.push('Hello'); + s.push('World'); + switch (counter) { + case 0: + s.emit('error', firstError); + counter++; + break; + case 1: + s.push('testing'); + s.push('retries'); + s.emit('status', expectedStatus); + counter++; + assert.deepStrictEqual( + receivedData.join(' '), + 'Hello World testing retries' + ); + done(); + break; + default: + break; + } + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // streaming retries + ); + + const s = apiCall( + {}, + { + retry: gax.createRetryOptions([14], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 1, + }), + } ); - const s = apiCall({}, undefined); - assert.strictEqual(s.readable, true); - assert.strictEqual(s.writable, false); - const actualResults: Array<{resources: Array}> = []; s.on('data', data => { - actualResults.push(data); + receivedData.push(data); + }); + }); + + it('emit error and retry twice with shouldRetryFn', done => { + // stubbing cancel is needed because PassThrough doesn't have + // a cancel method and cancel is called as part of the retry + sinon.stub(streaming.StreamProxy.prototype, 'cancel').callsFake(() => { + done(); + }); + const firstError = Object.assign(new GoogleError('UNAVAILABLE'), { + code: 14, + details: 'UNAVAILABLE', + }); + let counter = 0; + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + + switch (counter) { + case 0: + setImmediate(() => { + s.emit('error', firstError); + }); + setImmediate(() => { + s.emit('status'); + }); + counter++; + return s; + case 1: + setImmediate(() => { + s.emit('error', firstError); + }); + setImmediate(() => { + s.emit('status'); + }); + counter++; + return s; + default: + setImmediate(() => { + s.emit('end'); + }); + return s; + } + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // new retries + ); + + function shouldRetryFn(error: GoogleError) { + return [14].includes(error.code!); + } + const s = apiCall( + {}, + { + retry: gax.createRetryOptions( + [], + { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 2, // maxRetries must be > 1 to ensure we hit both checks for a shouldRetry function + }, + shouldRetryFn + ), + } + ); + + s.on('end', () => { + s.destroy(); + assert.strictEqual(counter, 2); + }); + }); + it('retries using resumption request function ', done => { + // stubbing cancel is needed because PassThrough doesn't have + // a cancel method and cancel is called as part of the retry + sinon.stub(streaming.StreamProxy.prototype, 'cancel'); + const receivedData: string[] = []; + const error = Object.assign(new GoogleError('test error'), { + code: 14, + details: 'UNAVAILABLE', + metadata: new Metadata(), + }); + + const spy = sinon.spy((...args: Array<{}>) => { + //@ts-ignore + const arg = args[0].arg; + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + switch (arg) { + case 0: + s.push('Hello'); + s.push('World'); + setImmediate(() => { + s.emit('metadata'); + }); + setImmediate(() => { + s.emit('error', error); + }); + setImmediate(() => { + s.emit('status'); + }); + return s; + case 1: + s.push(null); + setImmediate(() => { + s.emit('error', new Error('Should not reach')); + }); + + setImmediate(() => { + s.emit('status'); + }); + return s; + case 2: + s.push('testing'); + s.push('retries'); + setImmediate(() => { + s.emit('metadata'); + }); + setImmediate(() => { + s.emit('end'); + }); + return s; + default: + setImmediate(() => { + s.emit('end'); + }); + return s; + } + }); + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // new retry behavior enabled + ); + // resumption strategy is to pass a different arg to the function + const getResumptionRequestFn = (originalRequest: RequestType) => { + assert.strictEqual(originalRequest.arg, 0); + return {arg: 2}; + }; + const s = apiCall( + {arg: 0}, + { + retry: gax.createRetryOptions( + [14], + { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 2, // max retries or timeout must be > 0 in order to reach the code we want to test + }, + undefined, + getResumptionRequestFn + ), + } + ); + s.on('data', data => { + receivedData.push(data); + }); + s.on('error', err => { + // double check it's the expected error on the stream + // stream will continue after retry + assert(err instanceof GoogleError); + assert.deepStrictEqual(err.message, 'test error'); + }); + s.on('end', () => { + assert.strictEqual(receivedData.length, 4); + assert.deepStrictEqual( + receivedData.join(' '), + 'Hello World testing retries' + ); + done(); + }); + }); +}); + +describe('handles server streaming retries in gax when gaxStreamingRetries is enabled', () => { + afterEach(() => { + sinon.restore(); + }); + + it('server streaming call retries until exceeding max retries', done => { + const retrySpy = sinon.spy(streaming.StreamProxy.prototype, 'retry'); + const firstError = Object.assign(new GoogleError('UNAVAILABLE'), { + code: 14, + details: 'UNAVAILABLE', + metadata: new Metadata(), + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push('hello'); + setImmediate(() => { + s.emit('metadata'); + }); + setImmediate(() => { + s.emit('error', firstError); + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true + ); + + const call = apiCall( + {}, + { + retry: gax.createRetryOptions([14], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 2, + }), + } + ); + + call.on('error', err => { + assert(err instanceof GoogleError); + if (err.code !== 14) { + // ignore the error we are expecting + assert.strictEqual(err.code, 4); + // even though max retries is 2 + // the retry function will always be called maxRetries+1 + // the final call is where the failure happens + assert.strictEqual(retrySpy.callCount, 3); + assert.strictEqual( + err.message, + 'Exceeded maximum number of retries before any response was received' + ); + done(); + } + }); + }); + + it('server streaming call retries until exceeding total timeout', done => { + const firstError = Object.assign(new GoogleError('UNAVAILABLE'), { + code: 14, + details: 'UNAVAILABLE', + metadata: new Metadata(), + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push('hello'); + setImmediate(() => { + s.emit('metadata'); + }); + setImmediate(() => { + s.emit('error', firstError); + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true + ); + + const call = apiCall( + {}, + { + retry: gax.createRetryOptions([14], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 10, + }), + } + ); + + call.on('error', err => { + assert(err instanceof GoogleError); + if (err.code !== 14) { + // ignore the error we are expecting + assert.strictEqual(err.code, 4); + assert.strictEqual( + err.message, + 'Total timeout of API exceeded 10 milliseconds before any response was received.' + ); + done(); + } + }); + }); + it('allows custom CallOptions.retry settings with shouldRetryFn instead of retryCodes and new retry behavior', done => { + sinon + .stub(streaming.StreamProxy.prototype, 'forwardEventsWithRetries') + .callsFake((stream): undefined => { + assert(stream instanceof internal.Stream); + done(); + }); + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true //gaxStreamingRetries + ); + + apiCall( + {}, + { + retry: gax.createRetryOptions( + [], + { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }, + () => { + return true; + } + ), + } + ); + }); + it('allows custom CallOptions.retry settings with retryCodes and new retry behavior', done => { + sinon + .stub(streaming.StreamProxy.prototype, 'forwardEventsWithRetries') + .callsFake((stream): undefined => { + assert(stream instanceof internal.Stream); + done(); + }); + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true //gaxStreamingRetries + ); + + apiCall( + {}, + { + retry: gax.createRetryOptions([1, 2, 3], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + }); + it('allows the user to pass a custom resumption strategy', done => { + sinon + .stub(streaming.StreamProxy.prototype, 'forwardEventsWithRetries') + .callsFake((stream, retry): undefined => { + assert(stream instanceof internal.Stream); + assert(retry.getResumptionRequestFn instanceof Function); + done(); + }); + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true //gaxStreamingRetries + ); + + // "resumption" strategy is to just return the original request + const getResumptionRequestFn = (originalRequest: RequestType) => { + return originalRequest; + }; + + apiCall( + {}, + { + retry: gax.createRetryOptions( + [1, 2, 3], + { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }, + undefined, + getResumptionRequestFn + ), + } + ); + }); + it('throws an error when both totalTimeoutMillis and maxRetries are passed at call time when new retry behavior is enabled', done => { + const status = {code: 4, message: 'test'}; + const error = Object.assign(new GoogleError('test error'), { + code: 4, + details: 'Failed to read', + }); + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push(null); + setImmediate(() => { + s.emit('metadata'); + }); + setImmediate(() => { + // emits an error not in our included retry codes + s.emit('error', error); + }); + setImmediate(() => { + s.emit('status', status); + }); + + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // ensure we're doing the new retries + ); + + // make the call with both options passed at call time + const call = apiCall( + {}, + { + retry: gax.createRetryOptions([1, 4], { + initialRetryDelayMillis: 300, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4000, + maxRetries: 5, + }), + } + ); + call.on('error', err => { + assert(err instanceof GoogleError); + if (err.code !== 4) { + assert.strictEqual(err.code, 3); + assert.strictEqual( + err.message, + 'Cannot set both totalTimeoutMillis and maxRetries in backoffSettings.' + ); + done(); + } + }); + }); + it('throws an error when both retryRequestoptions and retryOptions are passed at call time when new retry behavior is enabled', done => { + //if this is reached, it means the settings merge in createAPICall did not fail properly + sinon.stub(StreamingApiCaller.prototype, 'call').callsFake(() => { + throw new Error("This shouldn't be happening"); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // ensure we're doing the new retries + ); + + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + // make the call with both options passed at call time + try { + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + retry: gax.createRetryOptions([1], { + initialRetryDelayMillis: 300, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.toString(), + 'Error: Only one of retry or retryRequestOptions may be set' + ); + done(); + } + }); + it('throws a warning and converts retryRequestOptions for new retry behavior', done => { + const warnStub = sinon.stub(warnings, 'warn'); + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + assert(typeof argument === 'object'); + assert(typeof apiCall === 'function'); + assert(stream instanceof streaming.StreamProxy); + try { + assert(settings.retry); + assert(typeof settings.retryRequestOptions === 'undefined'); + assert.strictEqual( + settings.retry?.backoffSettings.maxRetryDelayMillis, + 70000 + ); + assert.strictEqual( + settings.retry?.backoffSettings.retryDelayMultiplier, + 3 + ); + // totalTimeout is undefined because maxRetries is passed + assert( + typeof settings.retry?.backoffSettings.totalTimeoutMillis === + 'undefined' + ); + + assert.strictEqual(settings.retry?.backoffSettings.maxRetries, 1); + assert(settings.retry.shouldRetryFn); + assert(settings.retry.retryCodes.length === 0); + assert(settings.retry !== new gax.CallSettings().retry); + done(); + } catch (err) { + done(err); + } + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // gaxStreamingRetries + ); + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + + assert.strictEqual(warnStub.callCount, 4); + assert( + warnStub.calledWith( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'noResponseRetries override is not supported. Please specify retry codes or a function to determine retry eligibility.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'currentRetryAttempt override is not supported. Retry attempts are tracked internally.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'objectMode override is not supported. It is set to true internally by default in gax.', + 'UnsupportedParameterWarning' + ) + ); + }); + it('throws a warning and converts retryRequestOptions for new retry behavior - zero/falsiness check', done => { + const warnStub = sinon.stub(warnings, 'warn'); + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + assert(typeof argument === 'object'); + assert(typeof apiCall === 'function'); + assert(stream instanceof streaming.StreamProxy); + try { + assert(settings.retry); + assert(typeof settings.retryRequestOptions === 'undefined'); + assert.strictEqual( + settings.retry?.backoffSettings.maxRetryDelayMillis, + 0 + ); + assert.strictEqual( + settings.retry?.backoffSettings.retryDelayMultiplier, + 0 + ); + // totalTimeout is undefined because maxRetries is passed + assert( + typeof settings.retry?.backoffSettings.totalTimeoutMillis === + 'undefined' + ); + + assert.strictEqual(settings.retry?.backoffSettings.maxRetries, 0); + assert(settings.retry.shouldRetryFn); + assert(settings.retry.retryCodes.length === 0); + assert(settings.retry !== new gax.CallSettings().retry); + done(); + } catch (err) { + done(err); + } + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; }); - s.on('end', () => { - assert.strictEqual( - JSON.stringify(actualResults), - JSON.stringify([{resources: [1, 2]}, {resources: [3, 4, 5]}]) - ); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // gaxStreamingRetries + ); + const passedRetryRequestOptions = { + objectMode: false, + retries: 0, + maxRetryDelay: 0, + retryDelayMultiplier: 0, + totalTimeout: 0, + noResponseRetries: 0, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + + assert.strictEqual(warnStub.callCount, 4); + assert( + warnStub.calledWith( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'noResponseRetries override is not supported. Please specify retry codes or a function to determine retry eligibility.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'currentRetryAttempt override is not supported. Retry attempts are tracked internally.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'objectMode override is not supported. It is set to true internally by default in gax.', + 'UnsupportedParameterWarning' + ) + ); + }); + it('throws a warning and converts retryRequestOptions for new retry behavior - no maxRetries', done => { + const warnStub = sinon.stub(warnings, 'warn'); + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + assert(typeof argument === 'object'); + assert(typeof apiCall === 'function'); + assert(stream instanceof streaming.StreamProxy); + try { + assert(settings.retry); + assert(typeof settings.retryRequestOptions === 'undefined'); + assert.strictEqual( + settings.retry?.backoffSettings.maxRetryDelayMillis, + 70000 + ); + assert.strictEqual( + settings.retry?.backoffSettings.retryDelayMultiplier, + 3 + ); + assert.strictEqual( + settings.retry?.backoffSettings.totalTimeoutMillis, + 650000 + ); + assert( + typeof settings.retry?.backoffSettings.maxRetries === 'undefined' + ); + assert(settings.retry.shouldRetryFn); + assert(settings.retry.retryCodes.length === 0); + assert(settings.retry !== new gax.CallSettings().retry); + done(); + } catch (err) { + done(err); + } + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // gaxStreamingRetries + ); + const passedRetryRequestOptions = { + objectMode: false, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + + assert.strictEqual(warnStub.callCount, 4); + assert( + warnStub.calledWith( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'objectMode override is not supported. It is set to true internally by default in gax.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'noResponseRetries override is not supported. Please specify retry codes or a function to determine retry eligibility.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'currentRetryAttempt override is not supported. Retry attempts are tracked internally.', + 'UnsupportedParameterWarning' + ) + ); + }); + it('throws a warning and converts retryRequestOptions for new retry behavior - no maxRetries zero/falsiness check', done => { + const warnStub = sinon.stub(warnings, 'warn'); + sinon + .stub(StreamingApiCaller.prototype, 'call') + .callsFake((apiCall, argument, settings, stream) => { + assert(typeof argument === 'object'); + assert(typeof apiCall === 'function'); + assert(stream instanceof streaming.StreamProxy); + try { + assert(settings.retry); + assert(typeof settings.retryRequestOptions === 'undefined'); + assert.strictEqual( + settings.retry?.backoffSettings.maxRetryDelayMillis, + 0 + ); + assert.strictEqual( + settings.retry?.backoffSettings.retryDelayMultiplier, + 0 + ); + assert.strictEqual( + settings.retry?.backoffSettings.totalTimeoutMillis, + 0 + ); + assert( + typeof settings.retry?.backoffSettings.maxRetries === 'undefined' + ); + assert(settings.retry.shouldRetryFn); + assert(settings.retry.retryCodes.length === 0); + assert(settings.retry !== new gax.CallSettings().retry); + done(); + } catch (err) { + done(err); + } + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true // gaxStreamingRetries + ); + const passedRetryRequestOptions = { + objectMode: false, + maxRetryDelay: 0, + retryDelayMultiplier: 0, + totalTimeout: 0, + noResponseRetries: 0, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + + assert.strictEqual(warnStub.callCount, 4); + assert( + warnStub.calledWith( + 'retry_request_options', + 'retryRequestOptions will be deprecated in a future release. Please use retryOptions to pass retry options at call time', + 'DeprecationWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'objectMode override is not supported. It is set to true internally by default in gax.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'noResponseRetries override is not supported. Please specify retry codes or a function to determine retry eligibility.', + 'UnsupportedParameterWarning' + ) + ); + assert( + warnStub.calledWith( + 'retry_request_options', + 'currentRetryAttempt override is not supported. Retry attempts are tracked internally.', + 'UnsupportedParameterWarning' + ) + ); + }); +}); +describe('warns/errors about server streaming retry behavior when gaxStreamingRetries is disabled', () => { + afterEach(() => { + // restore 'call' stubs and 'warn' stubs + sinon.restore(); + }); + + it('throws a warning when retryRequestOptions are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + + // this exists to help resolve createApiCall + sinon.stub(StreamingApiCaller.prototype, 'call').callsFake(() => { + done(); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + // make the call with both options passed at call time + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + } + ); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_request_behavior', + 'Legacy streaming retry behavior will not honor retryRequestOptions passed at call time. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will convert retryRequestOptions to retry parameters by default in future releases.', + 'DeprecationWarning' + ) + ); + }); + it('throws a warning when retry options are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // this exists to help resolve createApiCall + sinon.stub(StreamingApiCaller.prototype, 'call').callsFake(() => { done(); }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + + // make the call with both options passed at call time + apiCall( + {}, + { + retry: gax.createRetryOptions([1], { + initialRetryDelayMillis: 300, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + assert.strictEqual(warnStub.callCount, 1); + assert( + warnStub.calledWith( + 'legacy_streaming_retry_behavior', + 'Legacy streaming retry behavior will not honor settings passed at call time or via client configuration. Please set gaxStreamingRetries to true to utilize passed retry settings. gaxStreamingRetries behavior will be set to true by default in future releases.', + 'DeprecationWarning' + ) + ); }); + it('throws no warnings when when no retry options are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // this exists to help resolve createApiCall + sinon.stub(StreamingApiCaller.prototype, 'call').callsFake(() => { + done(); + }); - it('forwards error event', done => { + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + + // make the call with neither retry option passed at call time + apiCall({}, {}); + assert.strictEqual(warnStub.callCount, 0); + }); + it('throws two warnings when when retry and retryRequestoptions are passed', done => { + const warnStub = sinon.stub(warnings, 'warn'); + // this exists to help resolve createApiCall + sinon.stub(StreamingApiCaller.prototype, 'call').callsFake(() => { + done(); + }); + + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + false // ensure we are NOT opted into the new retry behavior + ); + const passedRetryRequestOptions = { + objectMode: false, + retries: 1, + maxRetryDelay: 70, + retryDelayMultiplier: 3, + totalTimeout: 650, + noResponseRetries: 3, + currentRetryAttempt: 0, + shouldRetryFn: function alwaysRetry() { + return true; + }, + }; + // make the call with both retry options passed at call time + apiCall( + {}, + { + retryRequestOptions: passedRetryRequestOptions, + retry: gax.createRetryOptions([1], { + initialRetryDelayMillis: 300, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + totalTimeoutMillis: 4500, + }), + } + ); + assert.strictEqual(warnStub.callCount, 2); + }); +}); + +describe('REST streaming apiCall return StreamArrayParser', () => { + const protos_path = path.resolve(__dirname, '..', 'fixtures', 'user.proto'); + const root = protobuf.loadSync(protos_path); + const UserService = root.lookupService('UserService'); + UserService.resolveAll(); + const streamMethod = UserService.methods['RunQuery']; + it('forwards data, end event', done => { const spy = sinon.spy((...args: Array<{}>) => { assert.strictEqual(args.length, 3); const s = new StreamArrayParser(streamMethod); s.push({resources: [1, 2]}); + s.push({resources: [3, 4, 5]}); s.push(null); - s.emit('error', new Error('test error')); return s; }); const apiCall = createApiCallStreaming( @@ -571,46 +2178,40 @@ describe('REST streaming apiCall return StreamArrayParser', () => { const s = apiCall({}, undefined); assert.strictEqual(s.readable, true); assert.strictEqual(s.writable, false); - s.on('error', err => { - assert(err instanceof Error); - assert.deepStrictEqual(err.message, 'test error'); + const actualResults: Array<{resources: Array}> = []; + s.on('data', data => { + actualResults.push(data); + }); + s.on('end', () => { + assert.strictEqual( + JSON.stringify(actualResults), + JSON.stringify([{resources: [1, 2]}, {resources: [3, 4, 5]}]) + ); done(); }); }); - it('cancels StreamArrayParser in the middle', done => { - function schedulePush(s: StreamArrayParser, c: number) { - const intervalId = setInterval(() => { - s.push(c); - c++; - }, 10); - s.on('finish', () => { - clearInterval(intervalId); - }); - } + it('forwards error event', done => { const spy = sinon.spy((...args: Array<{}>) => { assert.strictEqual(args.length, 3); const s = new StreamArrayParser(streamMethod); - schedulePush(s, 0); + s.push({resources: [1, 2]}); + s.push(null); + s.emit('error', new Error('test error')); return s; }); const apiCall = createApiCallStreaming( - //@ts-ignore spy, streaming.StreamType.SERVER_STREAMING, true ); const s = apiCall({}, undefined); - let counter = 0; - const expectedCount = 5; - s.on('data', data => { - assert.strictEqual(data, counter); - counter++; - if (counter === expectedCount) { - s.cancel(); - } else if (counter > expectedCount) { - done(new Error('should not reach')); - } + assert.strictEqual(s.readable, true); + assert.strictEqual(s.writable, false); + s.on('error', err => { + assert(err instanceof Error); + assert.deepStrictEqual(err.message, 'test error'); + done(); }); s.on('end', () => { done(); diff --git a/gax/test/unit/streamingRetryRequest.ts b/gax/test/unit/streamingRetryRequest.ts new file mode 100644 index 000000000..5dca3ef32 --- /dev/null +++ b/gax/test/unit/streamingRetryRequest.ts @@ -0,0 +1,111 @@ +// Copyright 2023 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 + +// https://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. +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import {describe, it} from 'mocha'; +import {PassThrough} from 'stream'; + +import {CancellableStream, GaxCallStream, GRPCCall} from '../../src/apitypes'; +import {createApiCall} from '../../src/createApiCall'; +import * as gax from '../../src/gax'; +import {StreamDescriptor} from '../../src/streamingCalls/streamDescriptor'; +import * as streaming from '../../src/streamingCalls/streaming'; +import internal = require('stream'); +import {StreamArrayParser} from '../../src/streamArrayParser'; +import {streamingRetryRequest} from '../../src/streamingRetryRequest'; + +function createApiCallStreaming( + func: + | Promise + | sinon.SinonSpy, internal.Transform | StreamArrayParser>, + type: streaming.StreamType, + rest?: boolean, + gaxStreamingRetries?: boolean +) { + const settings = new gax.CallSettings(); + return createApiCall( + //@ts-ignore + Promise.resolve(func), + settings, + new StreamDescriptor(type, rest, gaxStreamingRetries) + ) as GaxCallStream; +} + +describe('retry-request', () => { + describe('streams', () => { + let receivedData: number[] = []; + it('works with defaults in a stream', done => { + const spy = sinon.spy((...args: Array<{}>) => { + assert.strictEqual(args.length, 3); + const s = new PassThrough({ + objectMode: true, + }); + s.push({resources: [1, 2]}); + s.push({resources: [3, 4, 5]}); + s.push(null); + setImmediate(() => { + s.emit('metadata'); + }); + return s; + }); + + const apiCall = createApiCallStreaming( + spy, + streaming.StreamType.SERVER_STREAMING, + false, + true + ); + + const retryStream = streamingRetryRequest({ + request: () => { + const stream = apiCall( + {}, + { + retry: gax.createRetryOptions([5], { + initialRetryDelayMillis: 100, + retryDelayMultiplier: 1.2, + maxRetryDelayMillis: 1000, + rpcTimeoutMultiplier: 1.5, + maxRpcTimeoutMillis: 3000, + maxRetries: 0, + }), + } + ) as CancellableStream; + return stream; + }, + }) + .on('end', () => { + assert.deepStrictEqual(receivedData, [1, 2, 3, 4, 5]); + done(); + }) + .on('data', (data: {resources: number[]}) => { + receivedData = receivedData.concat(data.resources); + }); + assert.strictEqual(retryStream._readableState.objectMode, true); + }); + + it('throws request error', done => { + try { + const opts = {}; + streamingRetryRequest(opts); + } catch (err) { + assert(err instanceof Error); + assert.match(err.message, /A request library must be provided/); + done(); + } + }); + }); +});