Skip to content

Commit 8ee8e99

Browse files
committed
grpc-js: Add a channel option to configure retry attempt limits
1 parent f867643 commit 8ee8e99

File tree

4 files changed

+58
-5
lines changed

4 files changed

+58
-5
lines changed

Diff for: packages/grpc-js/src/channel-options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface ChannelOptions {
6363
*/
6464
'grpc-node.tls_enable_trace'?: number;
6565
'grpc.lb.ring_hash.ring_size_cap'?: number;
66+
'grpc-node.retry_max_attempts_limit'?: number;
6667
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6768
[key: string]: any;
6869
}
@@ -99,6 +100,7 @@ export const recognizedOptions = {
99100
'grpc.client_idle_timeout_ms': true,
100101
'grpc-node.tls_enable_trace': true,
101102
'grpc.lb.ring_hash.ring_size_cap': true,
103+
'grpc-node.retry_max_attempts_limit': true,
102104
};
103105

104106
export function channelOptionsEqual(

Diff for: packages/grpc-js/src/internal-channel.ts

+4
Original file line numberDiff line numberDiff line change
@@ -842,4 +842,8 @@ export class InternalChannel {
842842
propagateFlags
843843
);
844844
}
845+
846+
getOptions() {
847+
return this.options;
848+
}
845849
}

Diff for: packages/grpc-js/src/retrying-call.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ interface WriteBufferEntry {
172172

173173
const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts';
174174

175+
const DEFAULT_MAX_ATTEMPTS_LIMIT = 5;
176+
175177
export class RetryingCall implements Call, DeadlineInfoProvider {
176178
private state: RetryingCallState;
177179
private listener: InterceptingListener | null = null;
@@ -201,6 +203,7 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
201203
private initialRetryBackoffSec = 0;
202204
private nextRetryBackoffSec = 0;
203205
private startTime: Date;
206+
private maxAttempts: number;
204207
constructor(
205208
private readonly channel: InternalChannel,
206209
private readonly callConfig: CallConfig,
@@ -212,6 +215,7 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
212215
private readonly bufferTracker: MessageBufferTracker,
213216
private readonly retryThrottler?: RetryThrottler
214217
) {
218+
const maxAttemptsLimit = channel.getOptions()['grpc-node.retry_max_attempts_limit'] ?? DEFAULT_MAX_ATTEMPTS_LIMIT;
215219
if (callConfig.methodConfig.retryPolicy) {
216220
this.state = 'RETRY';
217221
const retryPolicy = callConfig.methodConfig.retryPolicy;
@@ -221,10 +225,13 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
221225
retryPolicy.initialBackoff.length - 1
222226
)
223227
);
228+
this.maxAttempts = Math.min(retryPolicy.maxAttempts, maxAttemptsLimit);
224229
} else if (callConfig.methodConfig.hedgingPolicy) {
225230
this.state = 'HEDGING';
231+
this.maxAttempts = Math.min(callConfig.methodConfig.hedgingPolicy.maxAttempts, maxAttemptsLimit);
226232
} else {
227233
this.state = 'TRANSPARENT_ONLY';
234+
this.maxAttempts = 1;
228235
}
229236
this.startTime = new Date();
230237
}
@@ -419,8 +426,7 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
419426
callback(false);
420427
return;
421428
}
422-
const retryPolicy = this.callConfig!.methodConfig.retryPolicy!;
423-
if (this.attempts >= Math.min(retryPolicy.maxAttempts, 5)) {
429+
if (this.attempts >= this.maxAttempts) {
424430
callback(false);
425431
return;
426432
}
@@ -596,8 +602,7 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
596602
if (!this.callConfig.methodConfig.hedgingPolicy) {
597603
return;
598604
}
599-
const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy;
600-
if (this.attempts >= Math.min(hedgingPolicy.maxAttempts, 5)) {
605+
if (this.attempts >= this.maxAttempts) {
601606
return;
602607
}
603608
this.attempts += 1;
@@ -616,7 +621,7 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
616621
return;
617622
}
618623
const hedgingPolicy = this.callConfig.methodConfig.hedgingPolicy;
619-
if (this.attempts >= Math.min(hedgingPolicy.maxAttempts, 5)) {
624+
if (this.attempts >= this.maxAttempts) {
620625
return;
621626
}
622627
const hedgingDelayString = hedgingPolicy.hedgingDelay ?? '0s';

Diff for: packages/grpc-js/test/test-retry.ts

+42
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,48 @@ describe('Retries', () => {
281281
}
282282
);
283283
});
284+
285+
it('Should be able to make more than 5 attempts with a channel argument', done => {
286+
const serviceConfig = {
287+
loadBalancingConfig: [],
288+
methodConfig: [
289+
{
290+
name: [
291+
{
292+
service: 'EchoService',
293+
},
294+
],
295+
retryPolicy: {
296+
maxAttempts: 10,
297+
initialBackoff: '0.1s',
298+
maxBackoff: '10s',
299+
backoffMultiplier: 1.2,
300+
retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'],
301+
},
302+
},
303+
],
304+
};
305+
const client2 = new EchoService(
306+
`localhost:${port}`,
307+
grpc.credentials.createInsecure(),
308+
{
309+
'grpc.service_config': JSON.stringify(serviceConfig),
310+
'grpc-node.retry_max_attempts_limit': 8
311+
}
312+
);
313+
const metadata = new grpc.Metadata();
314+
metadata.set('succeed-on-retry-attempt', '7');
315+
metadata.set('respond-with-status', `${grpc.status.RESOURCE_EXHAUSTED}`);
316+
client2.echo(
317+
{ value: 'test value', value2: 3 },
318+
metadata,
319+
(error: grpc.ServiceError, response: any) => {
320+
assert.ifError(error);
321+
assert.deepStrictEqual(response, { value: 'test value', value2: 3 });
322+
done();
323+
}
324+
);
325+
});
284326
});
285327

286328
describe('Client with hedging configured', () => {

0 commit comments

Comments
 (0)