/* * Copyright 2022 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import * as assert from 'assert'; import * as path from 'path'; import * as grpc from '../src'; import { loadProtoFile } from './common'; const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); const EchoService = loadProtoFile(protoFile) .EchoService as grpc.ServiceClientConstructor; const serviceImpl = { echo: ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData ) => { const succeedOnRetryAttempt = call.metadata.get('succeed-on-retry-attempt'); const previousAttempts = call.metadata.get('grpc-previous-rpc-attempts'); if ( succeedOnRetryAttempt.length === 0 || (previousAttempts.length > 0 && previousAttempts[0] === succeedOnRetryAttempt[0]) ) { callback(null, call.request); } else { const statusCode = call.metadata.get('respond-with-status'); const code = statusCode[0] ? Number.parseInt(statusCode[0] as string) : grpc.status.UNKNOWN; callback({ code: code, details: `Failed on retry ${previousAttempts[0] ?? 0}`, }); } }, }; describe('Retries', () => { let server: grpc.Server; let port: number; before(done => { server = new grpc.Server(); server.addService(EchoService.service, serviceImpl); server.bindAsync( 'localhost:0', grpc.ServerCredentials.createInsecure(), (error, portNumber) => { if (error) { done(error); return; } port = portNumber; server.start(); done(); } ); }); after(() => { server.forceShutdown(); }); describe('Client with retries disabled', () => { let client: InstanceType; before(() => { client = new EchoService( `localhost:${port}`, grpc.credentials.createInsecure(), { 'grpc.enable_retries': 0 } ); }); after(() => { client.close(); }); it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); it('Should fail if the server fails the first request', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '1'); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert.strictEqual(error.details, 'Failed on retry 0'); done(); } ); }); }); describe('Client with retries enabled but not configured', () => { let client: InstanceType; before(() => { client = new EchoService( `localhost:${port}`, grpc.credentials.createInsecure() ); }); after(() => { client.close(); }); it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); it('Should fail if the server fails the first request', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '1'); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert.strictEqual(error.details, 'Failed on retry 0'); done(); } ); }); }); describe('Client with retries configured', () => { let client: InstanceType; before(() => { const serviceConfig = { loadBalancingConfig: [], methodConfig: [ { name: [ { service: 'EchoService', }, ], retryPolicy: { maxAttempts: 3, initialBackoff: '0.1s', maxBackoff: '10s', backoffMultiplier: 1.2, retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'], }, }, ], }; client = new EchoService( `localhost:${port}`, grpc.credentials.createInsecure(), { 'grpc.service_config': JSON.stringify(serviceConfig) } ); }); after(() => { client.close(); }); it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); it('Should succeed with few required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); it('Should fail with many required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '4'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert.strictEqual(error.details, 'Failed on retry 2'); done(); } ); }); it('Should fail with a fatal status code', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.NOT_FOUND}`); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert.strictEqual(error.details, 'Failed on retry 0'); done(); } ); }); it('Should not be able to make more than 5 attempts', done => { const serviceConfig = { loadBalancingConfig: [], methodConfig: [ { name: [ { service: 'EchoService', }, ], retryPolicy: { maxAttempts: 10, initialBackoff: '0.1s', maxBackoff: '10s', backoffMultiplier: 1.2, retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'], }, }, ], }; const client2 = new EchoService( `localhost:${port}`, grpc.credentials.createInsecure(), { 'grpc.service_config': JSON.stringify(serviceConfig) } ); const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '6'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); client2.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert.strictEqual(error.details, 'Failed on retry 4'); done(); } ); }); it('Should be able to make more than 5 attempts with a channel argument', done => { const serviceConfig = { loadBalancingConfig: [], methodConfig: [ { name: [ { service: 'EchoService', }, ], retryPolicy: { maxAttempts: 10, initialBackoff: '0.1s', maxBackoff: '10s', backoffMultiplier: 1.2, retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'], }, }, ], }; const client2 = new EchoService( `localhost:${port}`, grpc.credentials.createInsecure(), { 'grpc.service_config': JSON.stringify(serviceConfig), 'grpc-node.retry_max_attempts_limit': 8 } ); const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '7'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); client2.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); it('Should not retry on custom error code', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', '300'); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert.strictEqual(error.code, 300); assert.strictEqual(error.details, 'Failed on retry 0'); done(); } ); }); }); describe('Client with hedging configured', () => { let client: InstanceType; before(() => { const serviceConfig = { loadBalancingConfig: [], methodConfig: [ { name: [ { service: 'EchoService', }, ], hedgingPolicy: { maxAttempts: 3, nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'], }, }, ], }; client = new EchoService( `localhost:${port}`, grpc.credentials.createInsecure(), { 'grpc.service_config': JSON.stringify(serviceConfig) } ); }); after(() => { client.close(); }); it('Should be able to make a basic request', done => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); it('Should succeed with few required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); it('Should fail with many required attempts', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '4'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert(error.details.startsWith('Failed on retry')); done(); } ); }); it('Should fail with a fatal status code', done => { const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '2'); metadata.set('respond-with-status', `${grpc.status.NOT_FOUND}`); client.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert(error.details.startsWith('Failed on retry')); done(); } ); }); it('Should not be able to make more than 5 attempts', done => { const serviceConfig = { loadBalancingConfig: [], methodConfig: [ { name: [ { service: 'EchoService', }, ], hedgingPolicy: { maxAttempts: 10, nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'], }, }, ], }; const client2 = new EchoService( `localhost:${port}`, grpc.credentials.createInsecure(), { 'grpc.service_config': JSON.stringify(serviceConfig) } ); const metadata = new grpc.Metadata(); metadata.set('succeed-on-retry-attempt', '6'); metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`); client2.echo( { value: 'test value', value2: 3 }, metadata, (error: grpc.ServiceError, response: any) => { assert(error); assert(error.details.startsWith('Failed on retry')); done(); } ); }); }); });