grpc-node/packages/grpc-js/test/test-retry-config.ts

317 lines
9.0 KiB
TypeScript

/*
* 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 assert = require('assert');
import { validateServiceConfig } from '../src/service-config';
function createRetryServiceConfig(retryConfig: object): object {
return {
loadBalancingConfig: [],
methodConfig: [
{
name: [
{
service: 'A',
method: 'B',
},
],
retryPolicy: retryConfig,
},
],
};
}
function createHedgingServiceConfig(hedgingConfig: object): object {
return {
loadBalancingConfig: [],
methodConfig: [
{
name: [
{
service: 'A',
method: 'B',
},
],
hedgingPolicy: hedgingConfig,
},
],
};
}
function createThrottlingServiceConfig(retryThrottling: object): object {
return {
loadBalancingConfig: [],
methodConfig: [],
retryThrottling: retryThrottling,
};
}
interface TestCase {
description: string;
config: object;
error: RegExp;
}
const validRetryConfig = {
maxAttempts: 2,
initialBackoff: '1s',
maxBackoff: '1s',
backoffMultiplier: 1,
retryableStatusCodes: [14, 'RESOURCE_EXHAUSTED'],
};
const RETRY_TEST_CASES: TestCase[] = [
{
description: 'omitted maxAttempts',
config: {
initialBackoff: '1s',
maxBackoff: '1s',
backoffMultiplier: 1,
retryableStatusCodes: [14],
},
error: /retry policy: maxAttempts must be an integer at least 2/,
},
{
description: 'a low maxAttempts',
config: { ...validRetryConfig, maxAttempts: 1 },
error: /retry policy: maxAttempts must be an integer at least 2/,
},
{
description: 'omitted initialBackoff',
config: {
maxAttempts: 2,
maxBackoff: '1s',
backoffMultiplier: 1,
retryableStatusCodes: [14],
},
error:
/retry policy: initialBackoff must be a string consisting of a positive integer or decimal followed by s/,
},
{
description: 'a non-numeric initialBackoff',
config: { ...validRetryConfig, initialBackoff: 'abcs' },
error:
/retry policy: initialBackoff must be a string consisting of a positive integer or decimal followed by s/,
},
{
description: 'an initialBackoff without an s',
config: { ...validRetryConfig, initialBackoff: '123' },
error:
/retry policy: initialBackoff must be a string consisting of a positive integer or decimal followed by s/,
},
{
description: 'omitted maxBackoff',
config: {
maxAttempts: 2,
initialBackoff: '1s',
backoffMultiplier: 1,
retryableStatusCodes: [14],
},
error:
/retry policy: maxBackoff must be a string consisting of a positive integer or decimal followed by s/,
},
{
description: 'a non-numeric maxBackoff',
config: { ...validRetryConfig, maxBackoff: 'abcs' },
error:
/retry policy: maxBackoff must be a string consisting of a positive integer or decimal followed by s/,
},
{
description: 'an maxBackoff without an s',
config: { ...validRetryConfig, maxBackoff: '123' },
error:
/retry policy: maxBackoff must be a string consisting of a positive integer or decimal followed by s/,
},
{
description: 'omitted backoffMultiplier',
config: {
maxAttempts: 2,
initialBackoff: '1s',
maxBackoff: '1s',
retryableStatusCodes: [14],
},
error: /retry policy: backoffMultiplier must be a number greater than 0/,
},
{
description: 'a negative backoffMultiplier',
config: { ...validRetryConfig, backoffMultiplier: -1 },
error: /retry policy: backoffMultiplier must be a number greater than 0/,
},
{
description: 'omitted retryableStatusCodes',
config: {
maxAttempts: 2,
initialBackoff: '1s',
maxBackoff: '1s',
backoffMultiplier: 1,
},
error: /retry policy: retryableStatusCodes is required/,
},
{
description: 'empty retryableStatusCodes',
config: { ...validRetryConfig, retryableStatusCodes: [] },
error: /retry policy: retryableStatusCodes must be non-empty/,
},
{
description: 'unknown status code name',
config: { ...validRetryConfig, retryableStatusCodes: ['abcd'] },
error: /retry policy: retryableStatusCodes value not a status code name/,
},
{
description: 'out of range status code number',
config: { ...validRetryConfig, retryableStatusCodes: [12345] },
error: /retry policy: retryableStatusCodes value not in status code range/,
},
];
const validHedgingConfig = {
maxAttempts: 2,
};
const HEDGING_TEST_CASES: TestCase[] = [
{
description: 'omitted maxAttempts',
config: {},
error: /hedging policy: maxAttempts must be an integer at least 2/,
},
{
description: 'a low maxAttempts',
config: { ...validHedgingConfig, maxAttempts: 1 },
error: /hedging policy: maxAttempts must be an integer at least 2/,
},
{
description: 'a non-numeric hedgingDelay',
config: { ...validHedgingConfig, hedgingDelay: 'abcs' },
error:
/hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/,
},
{
description: 'a hedgingDelay without an s',
config: { ...validHedgingConfig, hedgingDelay: '123' },
error:
/hedging policy: hedgingDelay must be a string consisting of a positive integer followed by s/,
},
{
description: 'unknown status code name',
config: { ...validHedgingConfig, nonFatalStatusCodes: ['abcd'] },
error: /hedging policy: nonFatalStatusCodes value not a status code name/,
},
{
description: 'out of range status code number',
config: { ...validHedgingConfig, nonFatalStatusCodes: [12345] },
error: /hedging policy: nonFatalStatusCodes value not in status code range/,
},
];
const validThrottlingConfig = {
maxTokens: 100,
tokenRatio: 0.1,
};
const THROTTLING_TEST_CASES: TestCase[] = [
{
description: 'omitted maxTokens',
config: { tokenRatio: 0.1 },
error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/,
},
{
description: 'a large maxTokens',
config: { ...validThrottlingConfig, maxTokens: 1001 },
error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/,
},
{
description: 'zero maxTokens',
config: { ...validThrottlingConfig, maxTokens: 0 },
error: /retryThrottling: maxTokens must be a number in \(0, 1000\]/,
},
{
description: 'omitted tokenRatio',
config: { maxTokens: 100 },
error: /retryThrottling: tokenRatio must be a number greater than 0/,
},
{
description: 'zero tokenRatio',
config: { ...validThrottlingConfig, tokenRatio: 0 },
error: /retryThrottling: tokenRatio must be a number greater than 0/,
},
];
describe('Retry configs', () => {
describe('Retry', () => {
it('Should accept a valid config', () => {
assert.doesNotThrow(() => {
validateServiceConfig(createRetryServiceConfig(validRetryConfig));
});
});
for (const testCase of RETRY_TEST_CASES) {
it(`Should reject ${testCase.description}`, () => {
assert.throws(() => {
validateServiceConfig(createRetryServiceConfig(testCase.config));
}, testCase.error);
});
}
});
describe('Hedging', () => {
it('Should accept valid configs', () => {
assert.doesNotThrow(() => {
validateServiceConfig(createHedgingServiceConfig(validHedgingConfig));
});
assert.doesNotThrow(() => {
validateServiceConfig(
createHedgingServiceConfig({
...validHedgingConfig,
hedgingDelay: '1s',
})
);
});
assert.doesNotThrow(() => {
validateServiceConfig(
createHedgingServiceConfig({
...validHedgingConfig,
nonFatalStatusCodes: [14, 'RESOURCE_EXHAUSTED'],
})
);
});
});
for (const testCase of HEDGING_TEST_CASES) {
it(`Should reject ${testCase.description}`, () => {
assert.throws(() => {
validateServiceConfig(createHedgingServiceConfig(testCase.config));
}, testCase.error);
});
}
});
describe('Throttling', () => {
it('Should accept a valid config', () => {
assert.doesNotThrow(() => {
validateServiceConfig(
createThrottlingServiceConfig(validThrottlingConfig)
);
});
});
for (const testCase of THROTTLING_TEST_CASES) {
it(`Should reject ${testCase.description}`, () => {
assert.throws(() => {
validateServiceConfig(createThrottlingServiceConfig(testCase.config));
}, testCase.error);
});
}
});
});