1
- import {
2
- InterceptingCall ,
3
- Interceptor ,
4
- ListenerBuilder ,
5
- Metadata ,
6
- RequesterBuilder ,
7
- StatusObject ,
8
- } from '@grpc/grpc-js' ;
1
+ import { InterceptingCall , Interceptor , ListenerBuilder , RequesterBuilder , StatusObject } from '@grpc/grpc-js' ;
9
2
import * as grpc from '@grpc/grpc-js' ;
10
3
11
4
export interface GrpcRetryOptions {
12
- /** Maximum number of allowed retries. Defaults to 10. */
13
- maxRetries : number ;
14
-
15
5
/**
16
- * A function which accepts the current retry attempt (starts at 0 ) and returns the millisecond
6
+ * A function which accepts the current retry attempt (starts at 1 ) and returns the millisecond
17
7
* delay that should be applied before the next retry.
18
8
*/
19
- delayFunction : ( attempt : number ) => number ;
9
+ delayFunction : ( attempt : number , status : StatusObject ) => number ;
20
10
21
11
/**
22
12
* A function which accepts a failed status object and returns true if the call should be retried
23
13
*/
24
- retryableDecider : ( status : StatusObject ) => boolean ;
14
+ retryableDecider : ( attempt : number , status : StatusObject ) => boolean ;
15
+ }
16
+
17
+ /**
18
+ * Options for the backoff formula: `factor ^ attempt * initialIntervalMs(status) * jitter(maxJitter)`
19
+ */
20
+ export interface BackoffOptions {
21
+ /**
22
+ * Exponential backoff factor
23
+ *
24
+ * @default 2
25
+ */
26
+ factor : number ;
27
+
28
+ /**
29
+ * Maximum number of attempts
30
+ *
31
+ * @default 10
32
+ */
33
+ maxAttempts : number ;
34
+ /**
35
+ * Maximum amount of jitter to apply
36
+ *
37
+ * @default 0.1
38
+ */
39
+ maxJitter : number ;
40
+ /**
41
+ * Function that returns the "initial" backoff interval based on the returned status.
42
+ *
43
+ * The default is 1 second for RESOURCE_EXHAUSTED errors and 20 millis for other retryable errors.
44
+ */
45
+ initialIntervalMs ( status : StatusObject ) : number ;
25
46
}
26
47
27
- export function defaultGrpcRetryOptions ( ) : GrpcRetryOptions {
48
+ /**
49
+ * Add defaults as documented in {@link BackoffOptions}
50
+ */
51
+ function withDefaultBackoffOptions ( {
52
+ maxAttempts,
53
+ factor,
54
+ maxJitter,
55
+ initialIntervalMs,
56
+ } : Partial < BackoffOptions > ) : BackoffOptions {
28
57
return {
29
- maxRetries : 10 ,
30
- delayFunction : backOffAmount ,
31
- retryableDecider : isRetryableError ,
58
+ maxAttempts : maxAttempts ?? 10 ,
59
+ factor : factor ?? 2 ,
60
+ maxJitter : maxJitter ?? 0.1 ,
61
+ initialIntervalMs : initialIntervalMs ?? defaultInitialIntervalMs ,
32
62
} ;
33
63
}
34
64
65
+ /**
66
+ * Generates the default retry behavior based on given backoff options
67
+ */
68
+ export function defaultGrpcRetryOptions ( options : Partial < BackoffOptions > = { } ) : GrpcRetryOptions {
69
+ const { maxAttempts, factor, maxJitter, initialIntervalMs } = withDefaultBackoffOptions ( options ) ;
70
+ return {
71
+ delayFunction ( attempt , status ) {
72
+ return factor ** attempt * initialIntervalMs ( status ) * jitter ( maxJitter ) ;
73
+ } ,
74
+ retryableDecider ( attempt , status ) {
75
+ return attempt < maxAttempts && isRetryableError ( status ) ;
76
+ } ,
77
+ } ;
78
+ }
79
+
80
+ /**
81
+ * Set of retryable gRPC status codes
82
+ */
35
83
const retryableCodes = new Set ( [
36
84
grpc . status . UNKNOWN ,
37
85
grpc . status . RESOURCE_EXHAUSTED ,
@@ -45,69 +93,75 @@ export function isRetryableError(status: StatusObject): boolean {
45
93
return retryableCodes . has ( status . code ) ;
46
94
}
47
95
48
- /** Return backoff amount in ms */
49
- export function backOffAmount ( attempt : number ) : number {
50
- return 2 ** attempt * 20 ;
96
+ /**
97
+ * Calculates random amount of jitter between 0 and `max`
98
+ */
99
+ function jitter ( max : number ) {
100
+ return 1 - max + Math . random ( ) * max * 2 ;
101
+ }
102
+
103
+ /**
104
+ * Default implementation - backs off more on RESOURCE_EXHAUSTED errors
105
+ */
106
+ function defaultInitialIntervalMs ( { code } : StatusObject ) {
107
+ // Backoff more on RESOURCE_EXHAUSTED
108
+ if ( code === grpc . status . RESOURCE_EXHAUSTED ) {
109
+ return 1000 ;
110
+ }
111
+ return 20 ;
51
112
}
52
113
53
114
/**
54
115
* Returns a GRPC interceptor that will perform automatic retries for some types of failed calls
55
116
*
56
117
* @param retryOptions Options for the retry interceptor
57
118
*/
58
- export function makeGrpcRetryInterceptor ( retryOptions : GrpcRetryOptions ) : Interceptor {
119
+ export function makeGrpcRetryInterceptor ( { retryableDecider , delayFunction } : GrpcRetryOptions ) : Interceptor {
59
120
return ( options , nextCall ) => {
60
- let savedMetadata : Metadata ;
61
121
let savedSendMessage : any ;
62
122
let savedReceiveMessage : any ;
63
- let savedMessageNext : any ;
123
+ let savedMessageNext : ( message : any ) => void ;
124
+
64
125
const requester = new RequesterBuilder ( )
65
126
. withStart ( function ( metadata , _listener , next ) {
66
- savedMetadata = metadata ;
67
- const newListener = new ListenerBuilder ( )
127
+ // First attempt
128
+ let attempt = 1 ;
129
+
130
+ const listener = new ListenerBuilder ( )
68
131
. withOnReceiveMessage ( ( message , next ) => {
69
132
savedReceiveMessage = message ;
70
133
savedMessageNext = next ;
71
134
} )
72
135
. withOnReceiveStatus ( ( status , next ) => {
73
- let retries = 0 ;
74
- const retry = ( message : any , metadata : Metadata ) => {
75
- retries ++ ;
76
- const newCall = nextCall ( options ) ;
77
- newCall . start ( metadata , {
78
- onReceiveMessage : ( message ) => {
136
+ const retry = ( ) => {
137
+ attempt ++ ;
138
+ const call = nextCall ( options ) ;
139
+ call . start ( metadata , {
140
+ onReceiveMessage ( message ) {
79
141
savedReceiveMessage = message ;
80
142
} ,
81
- onReceiveStatus : ( status ) => {
82
- if ( retryOptions . retryableDecider ( status ) ) {
83
- if ( retries <= retryOptions . maxRetries ) {
84
- setTimeout ( ( ) => retry ( message , metadata ) , retryOptions . delayFunction ( retries ) ) ;
85
- } else {
86
- savedMessageNext ( savedReceiveMessage ) ;
87
- next ( status ) ;
88
- }
89
- } else {
90
- savedMessageNext ( savedReceiveMessage ) ;
91
- // TODO: For reasons that are completely unclear to me, if you pass a handcrafted
92
- // status object here, node will magically just exit at the end of this line.
93
- // No warning, no nothing. Here be dragons.
94
- next ( status ) ;
95
- }
96
- } ,
143
+ onReceiveStatus,
97
144
} ) ;
98
- newCall . sendMessage ( message ) ;
99
- newCall . halfClose ( ) ;
145
+ call . sendMessage ( savedSendMessage ) ;
146
+ call . halfClose ( ) ;
147
+ } ;
148
+
149
+ const onReceiveStatus = ( status : StatusObject ) => {
150
+ if ( retryableDecider ( attempt , status ) ) {
151
+ setTimeout ( retry , delayFunction ( attempt , status ) ) ;
152
+ } else {
153
+ savedMessageNext ( savedReceiveMessage ) ;
154
+ // TODO: For reasons that are completely unclear to me, if you pass a handcrafted
155
+ // status object here, node will magically just exit at the end of this line.
156
+ // No warning, no nothing. Here be dragons.
157
+ next ( status ) ;
158
+ }
100
159
} ;
101
160
102
- if ( retryOptions . retryableDecider ( status ) ) {
103
- setTimeout ( ( ) => retry ( savedSendMessage , savedMetadata ) , backOffAmount ( retries ) ) ;
104
- } else {
105
- savedMessageNext ( savedReceiveMessage ) ;
106
- next ( status ) ;
107
- }
161
+ onReceiveStatus ( status ) ;
108
162
} )
109
163
. build ( ) ;
110
- next ( metadata , newListener ) ;
164
+ next ( metadata , listener ) ;
111
165
} )
112
166
. withSendMessage ( ( message , next ) => {
113
167
savedSendMessage = message ;
0 commit comments