Skip to content

Commit 4afe11a

Browse files
authored
feat: Add retry backoff to grpc connections (#1437)
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
1 parent d8104fa commit 4afe11a

File tree

5 files changed

+75
-11
lines changed

5 files changed

+75
-11
lines changed

libs/providers/flagd/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Options can be defined in the constructor or as environment variables. Construct
2828
### Available Configuration Options
2929

3030
| Option name | Environment variable name | Type | Default | Supported values |
31-
| -------------------------------------- | ------------------------------ | ------- |----------------------------------------------------------------| ---------------- |
31+
| -------------------------------------- | ------------------------------ | ------- |----------------------------------------------------------------|------------------|
3232
| host | FLAGD_HOST | string | localhost | |
3333
| port | FLAGD_PORT | number | [resolver specific defaults](#resolver-type-specific-defaults) | |
3434
| tls | FLAGD_TLS | boolean | false | |
@@ -40,7 +40,9 @@ Options can be defined in the constructor or as environment variables. Construct
4040
| cache | FLAGD_CACHE | string | lru | lru, disabled |
4141
| maxCacheSize | FLAGD_MAX_CACHE_SIZE | int | 1000 | |
4242
| defaultAuthority | FLAGD_DEFAULT_AUTHORITY | string | - | rpc, in-process |
43-
| keepAliveTime | FLAGD_KEEP_ALIVE_TIME_MS | number | 0 | rpc, in-process |
43+
| keepAliveTime | FLAGD_KEEP_ALIVE_TIME_MS | number | 0 | rpc, in-process |
44+
| retryBackoffMs | FLAGD_RETRY_BACKOFF_MS | int | 1000 | in-process |
45+
| retryBackoffMaxMs | FLAGD_RETRY_BACKOFF_MAX_MS | int | 120000 | in-process |
4446

4547
#### Resolver type-specific Defaults
4648

libs/providers/flagd/src/lib/configuration.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ describe('Configuration', () => {
2323
maxCacheSize: DEFAULT_MAX_CACHE_SIZE,
2424
cache: 'lru',
2525
resolverType: 'rpc',
26+
retryBackoffMaxMs: 120000,
27+
retryBackoffMs: 1000,
2628
selector: '',
2729
deadlineMs: 500,
2830
contextEnricher: expect.any(Function),
@@ -118,6 +120,8 @@ describe('Configuration', () => {
118120
maxCacheSize: 1000,
119121
cache: 'lru',
120122
resolverType: 'rpc',
123+
retryBackoffMaxMs: 120000,
124+
retryBackoffMs: 1000,
121125
selector: '',
122126
defaultAuthority: '',
123127
deadlineMs: 500,

libs/providers/flagd/src/lib/configuration.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ export interface Config {
9090
*/
9191
defaultAuthority?: string;
9292

93+
/**
94+
* Initial retry backoff in milliseconds.
95+
*/
96+
retryBackoffMs?: number;
97+
98+
/**
99+
* Maximum retry backoff in milliseconds.
100+
*/
101+
retryBackoffMaxMs?: number;
102+
93103
/**
94104
* gRPC client KeepAlive in milliseconds. Disabled with 0.
95105
* Only applies to RPC and in-process resolvers.
@@ -131,6 +141,8 @@ const DEFAULT_CONFIG: Omit<FlagdConfig, 'port' | 'resolverType'> = {
131141
cache: 'lru',
132142
maxCacheSize: DEFAULT_MAX_CACHE_SIZE,
133143
contextEnricher: (syncContext: EvaluationContext | null) => syncContext ?? {},
144+
retryBackoffMs: 1000,
145+
retryBackoffMaxMs: 120000,
134146
keepAliveTime: 0,
135147
retryGracePeriod: DEFAULT_RETRY_GRACE_PERIOD,
136148
};
@@ -153,6 +165,8 @@ enum ENV_VAR {
153165
FLAGD_RESOLVER = 'FLAGD_RESOLVER',
154166
FLAGD_OFFLINE_FLAG_SOURCE_PATH = 'FLAGD_OFFLINE_FLAG_SOURCE_PATH',
155167
FLAGD_DEFAULT_AUTHORITY = 'FLAGD_DEFAULT_AUTHORITY',
168+
FLAGD_RETRY_BACKOFF_MS = 'FLAGD_RETRY_BACKOFF_MS',
169+
FLAGD_RETRY_BACKOFF_MAX_MS = 'FLAGD_RETRY_BACKOFF_MAX_MS',
156170
FLAGD_KEEP_ALIVE_TIME_MS = 'FLAGD_KEEP_ALIVE_TIME_MS',
157171
FLAGD_RETRY_GRACE_PERIOD = 'FLAGD_RETRY_GRACE_PERIOD',
158172
}
@@ -215,6 +229,12 @@ const getEnvVarConfig = (): Partial<Config> => {
215229
...(process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY] && {
216230
defaultAuthority: process.env[ENV_VAR.FLAGD_DEFAULT_AUTHORITY],
217231
}),
232+
...(Number(process.env[ENV_VAR.FLAGD_RETRY_BACKOFF_MS]) && {
233+
retryBackoffMs: Number(process.env[ENV_VAR.FLAGD_RETRY_BACKOFF_MS]),
234+
}),
235+
...(Number(process.env[ENV_VAR.FLAGD_RETRY_BACKOFF_MAX_MS]) && {
236+
retryBackoffMaxMs: Number(process.env[ENV_VAR.FLAGD_RETRY_BACKOFF_MAX_MS]),
237+
}),
218238
...(Number(process.env[ENV_VAR.FLAGD_KEEP_ALIVE_TIME_MS]) && {
219239
keepAliveTime: Number(process.env[ENV_VAR.FLAGD_KEEP_ALIVE_TIME_MS]),
220240
}),

libs/providers/flagd/src/lib/service/common/grpc-util.spec.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { buildClientOptions } from './grpc-util';
1+
import { buildClientOptions, buildRetryPolicy } from './grpc-util';
22
import type { Config } from '../../configuration';
33

44
describe('buildClientOptions', () => {
@@ -8,10 +8,13 @@ describe('buildClientOptions', () => {
88
tls: false,
99
deadlineMs: 500,
1010
socketPath: '',
11+
retryBackoffMs: 100,
12+
retryBackoffMaxMs: 200,
1113
};
1214

13-
it('should return undefined when no relevant options are set', () => {
14-
expect(buildClientOptions(baseConfig)).toBeUndefined();
15+
it('should only return retry policy when no relevant options are set', () => {
16+
expect(Object.keys(buildClientOptions(baseConfig)).length).toBe(1);
17+
expect(Object.keys(buildClientOptions(baseConfig))).toEqual(['grpc.service_config']);
1518
});
1619

1720
it.each([
@@ -24,22 +27,23 @@ describe('buildClientOptions', () => {
2427
{ configKey: 'keepAliveTime', value: 10000, grpcKey: 'grpc.keepalive_time_ms', expected: 10000 },
2528
])('should include $configKey when set to valid value', ({ configKey, value, grpcKey, expected }) => {
2629
const config = { ...baseConfig, [configKey]: value };
27-
expect(buildClientOptions(config)).toEqual({ [grpcKey]: expected });
30+
expect(buildClientOptions(config)).toMatchObject({ [grpcKey]: expected });
2831
});
2932

3033
it.each([
3134
{ configKey: 'keepAliveTime', value: 0, description: 'zero' },
3235
{ configKey: 'keepAliveTime', value: -1, description: 'negative' },
3336
])('should exclude $configKey when $description', ({ configKey, value }) => {
3437
const config = { ...baseConfig, [configKey]: value };
35-
expect(buildClientOptions(config)).toBeUndefined();
38+
expect(Object.keys(buildClientOptions(config))).not.toContain('grpc.keepalive_time_ms');
3639
});
3740

3841
it('should combine multiple options', () => {
3942
const config: Config = { ...baseConfig, defaultAuthority: 'my-authority', keepAliveTime: 5000 };
4043
expect(buildClientOptions(config)).toEqual({
4144
'grpc.default_authority': 'my-authority',
4245
'grpc.keepalive_time_ms': 5000,
46+
'grpc.service_config': buildRetryPolicy('flagd.service.v1.FlagService', 100, 200),
4347
});
4448
});
4549
});

libs/providers/flagd/src/lib/service/common/grpc-util.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { credentials } from '@grpc/grpc-js';
1+
import { credentials, status } from '@grpc/grpc-js';
22
import type { ClientReadableStream, ChannelCredentials, ClientOptions } from '@grpc/grpc-js';
33
import { readFileSync, existsSync } from 'node:fs';
44
import type { Config } from '../../configuration';
@@ -48,8 +48,14 @@ const CONFIG_TO_GRPC_OPTIONS: {
4848
/**
4949
* Builds gRPC client options from config.
5050
*/
51-
export function buildClientOptions(config: Config): ClientOptions | undefined {
52-
const options: Partial<ClientOptions> = {};
51+
export function buildClientOptions(config: Config): ClientOptions {
52+
const options: Partial<ClientOptions> = {
53+
'grpc.service_config': buildRetryPolicy(
54+
'flagd.service.v1.FlagService',
55+
config.retryBackoffMs,
56+
config.retryBackoffMaxMs,
57+
),
58+
};
5359

5460
for (const { configKey, grpcKey, condition } of CONFIG_TO_GRPC_OPTIONS) {
5561
const value = config[configKey];
@@ -58,5 +64,33 @@ export function buildClientOptions(config: Config): ClientOptions | undefined {
5864
}
5965
}
6066

61-
return Object.keys(options).length > 0 ? options : undefined;
67+
return options;
6268
}
69+
70+
/**
71+
* Builds RetryPolicy for gRPC client options.
72+
* @param serviceName
73+
* @param retryBackoffMs Initial backoff duration in milliseconds
74+
* @param retryBackoffMaxMs Maximum backoff duration in milliseconds
75+
* @returns gRPC client options with retry policy
76+
*/
77+
export const buildRetryPolicy = (serviceName: string, retryBackoffMs?: number, retryBackoffMaxMs?: number): string => {
78+
const initialBackoff = retryBackoffMs ?? 1000;
79+
const maxBackoff = retryBackoffMaxMs ?? 120000;
80+
81+
return JSON.stringify({
82+
loadBalancingConfig: [],
83+
methodConfig: [
84+
{
85+
name: [{ service: serviceName }],
86+
retryPolicy: {
87+
maxAttempts: 3,
88+
initialBackoff: `${Math.round(initialBackoff / 1000).toFixed(2)}s`,
89+
maxBackoff: `${Math.round(maxBackoff / 1000).toFixed(2)}s`,
90+
backoffMultiplier: 2,
91+
retryableStatusCodes: [status.UNAVAILABLE, status.UNKNOWN],
92+
},
93+
},
94+
],
95+
});
96+
};

0 commit comments

Comments
 (0)