/* * 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); }); } }); });