/* * 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 * as assert from 'assert'; import * as path from 'path'; import * as grpc from '../src'; import { loadProtoFile } from './common'; import { OutlierDetectionLoadBalancingConfig } from '../src/load-balancer-outlier-detection'; function multiDone(done: Mocha.Done, target: number) { let count = 0; return (error?: any) => { if (error) { done(error); } count++; if (count >= target) { done(); } }; } const defaultOutlierDetectionServiceConfig = { methodConfig: [], loadBalancingConfig: [ { outlier_detection: { success_rate_ejection: {}, failure_percentage_ejection: {}, child_policy: [{ round_robin: {} }], }, }, ], }; const defaultOutlierDetectionServiceConfigString = JSON.stringify( defaultOutlierDetectionServiceConfig ); const successRateOutlierDetectionServiceConfig = { methodConfig: [], loadBalancingConfig: [ { outlier_detection: { interval: { seconds: 1, nanos: 0, }, base_ejection_time: { seconds: 3, nanos: 0, }, success_rate_ejection: { request_volume: 5, }, child_policy: [{ round_robin: {} }], }, }, ], }; const successRateOutlierDetectionServiceConfigString = JSON.stringify( successRateOutlierDetectionServiceConfig ); const failurePercentageOutlierDetectionServiceConfig = { methodConfig: [], loadBalancingConfig: [ { outlier_detection: { interval: { seconds: 1, nanos: 0, }, base_ejection_time: { seconds: 3, nanos: 0, }, failure_percentage_ejection: { request_volume: 5, }, child_policy: [{ round_robin: {} }], }, }, ], }; const falurePercentageOutlierDetectionServiceConfigString = JSON.stringify( failurePercentageOutlierDetectionServiceConfig ); const goodService = { echo: ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData ) => { callback(null, call.request); }, }; const badService = { echo: ( call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData ) => { callback({ code: grpc.status.PERMISSION_DENIED, details: 'Permission denied', }); }, }; const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); const EchoService = loadProtoFile(protoFile) .EchoService as grpc.ServiceClientConstructor; describe('Outlier detection config validation', () => { describe('interval', () => { it('Should reject a negative interval', () => { const loadBalancingConfig = { interval: { seconds: -1, nanos: 0, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /interval parse error: values out of range for non-negative Duaration/); }); it('Should reject a large interval', () => { const loadBalancingConfig = { interval: { seconds: 1e12, nanos: 0, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /interval parse error: values out of range for non-negative Duaration/); }); it('Should reject a negative interval.nanos', () => { const loadBalancingConfig = { interval: { seconds: 0, nanos: -1, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /interval parse error: values out of range for non-negative Duaration/); }); it('Should reject a large interval.nanos', () => { const loadBalancingConfig = { interval: { seconds: 0, nanos: 1e12, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /interval parse error: values out of range for non-negative Duaration/); }); }); describe('base_ejection_time', () => { it('Should reject a negative base_ejection_time', () => { const loadBalancingConfig = { base_ejection_time: { seconds: -1, nanos: 0, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /base_ejection_time parse error: values out of range for non-negative Duaration/); }); it('Should reject a large base_ejection_time', () => { const loadBalancingConfig = { base_ejection_time: { seconds: 1e12, nanos: 0, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /base_ejection_time parse error: values out of range for non-negative Duaration/); }); it('Should reject a negative base_ejection_time.nanos', () => { const loadBalancingConfig = { base_ejection_time: { seconds: 0, nanos: -1, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /base_ejection_time parse error: values out of range for non-negative Duaration/); }); it('Should reject a large base_ejection_time.nanos', () => { const loadBalancingConfig = { base_ejection_time: { seconds: 0, nanos: 1e12, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /base_ejection_time parse error: values out of range for non-negative Duaration/); }); }); describe('max_ejection_time', () => { it('Should reject a negative max_ejection_time', () => { const loadBalancingConfig = { max_ejection_time: { seconds: -1, nanos: 0, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /max_ejection_time parse error: values out of range for non-negative Duaration/); }); it('Should reject a large max_ejection_time', () => { const loadBalancingConfig = { max_ejection_time: { seconds: 1e12, nanos: 0, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /max_ejection_time parse error: values out of range for non-negative Duaration/); }); it('Should reject a negative max_ejection_time.nanos', () => { const loadBalancingConfig = { max_ejection_time: { seconds: 0, nanos: -1, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /max_ejection_time parse error: values out of range for non-negative Duaration/); }); it('Should reject a large max_ejection_time.nanos', () => { const loadBalancingConfig = { max_ejection_time: { seconds: 0, nanos: 1e12, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /max_ejection_time parse error: values out of range for non-negative Duaration/); }); }); describe('max_ejection_percent', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { max_ejection_percent: 101, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /max_ejection_percent parse error: value out of range for percentage/); }); it('Should reject a negative value', () => { const loadBalancingConfig = { max_ejection_percent: -1, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /max_ejection_percent parse error: value out of range for percentage/); }); }); describe('success_rate_ejection.enforcement_percentage', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { success_rate_ejection: { enforcement_percentage: 101, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /success_rate_ejection\.enforcement_percentage parse error: value out of range for percentage/); }); it('Should reject a negative value', () => { const loadBalancingConfig = { success_rate_ejection: { enforcement_percentage: -1, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /success_rate_ejection\.enforcement_percentage parse error: value out of range for percentage/); }); }); describe('failure_percentage_ejection.threshold', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { failure_percentage_ejection: { threshold: 101, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /failure_percentage_ejection\.threshold parse error: value out of range for percentage/); }); it('Should reject a negative value', () => { const loadBalancingConfig = { failure_percentage_ejection: { threshold: -1, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /failure_percentage_ejection\.threshold parse error: value out of range for percentage/); }); }); describe('failure_percentage_ejection.enforcement_percentage', () => { it('Should reject a value above 100', () => { const loadBalancingConfig = { failure_percentage_ejection: { enforcement_percentage: 101, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /failure_percentage_ejection\.enforcement_percentage parse error: value out of range for percentage/); }); it('Should reject a negative value', () => { const loadBalancingConfig = { failure_percentage_ejection: { enforcement_percentage: -1, }, child_policy: [{ round_robin: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /failure_percentage_ejection\.enforcement_percentage parse error: value out of range for percentage/); }); }); describe('child_policy', () => { it('Should reject a pick_first child_policy', () => { const loadBalancingConfig = { child_policy: [{ pick_first: {} }], }; assert.throws(() => { OutlierDetectionLoadBalancingConfig.createFromJson(loadBalancingConfig); }, /outlier_detection LB policy cannot have a pick_first child policy/); }); }); }); describe('Outlier detection', () => { const GOOD_PORTS = 4; let goodServer: grpc.Server; let badServer: grpc.Server; const goodPorts: number[] = []; let badPort: number; before(done => { const eachDone = multiDone(() => { goodServer.start(); badServer.start(); done(); }, GOOD_PORTS + 1); goodServer = new grpc.Server(); goodServer.addService(EchoService.service, goodService); for (let i = 0; i < GOOD_PORTS; i++) { goodServer.bindAsync( 'localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { if (error) { eachDone(error); return; } goodPorts.push(port); eachDone(); } ); } badServer = new grpc.Server(); badServer.addService(EchoService.service, badService); badServer.bindAsync( 'localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => { if (error) { eachDone(error); return; } badPort = port; eachDone(); } ); }); after(() => { goodServer.forceShutdown(); badServer.forceShutdown(); }); function makeManyRequests( makeOneRequest: (callback: (error?: Error) => void) => void, total: number, callback: (error?: Error) => void ) { if (total === 0) { callback(); return; } makeOneRequest(error => { if (error) { callback(error); return; } makeManyRequests(makeOneRequest, total - 1, callback); }); } it('Should allow normal operation with one server', done => { const client = new EchoService( `localhost:${goodPorts[0]}`, grpc.credentials.createInsecure(), { 'grpc.service_config': defaultOutlierDetectionServiceConfigString } ); client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { assert.ifError(error); assert.deepStrictEqual(response, { value: 'test value', value2: 3 }); done(); } ); }); describe('Success rate', () => { let makeCheckedRequest: (callback: () => void) => void; let makeUncheckedRequest: (callback: (error?: Error) => void) => void; before(() => { const target = 'ipv4:///' + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + `,127.0.0.1:${badPort}`; const client = new EchoService( target, grpc.credentials.createInsecure(), { 'grpc.service_config': successRateOutlierDetectionServiceConfigString, } ); makeUncheckedRequest = (callback: () => void) => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { callback(); } ); }; makeCheckedRequest = (callback: (error?: Error) => void) => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { callback(error); } ); }; }); it('Should eject a server if it is failing requests', done => { // Make a large volume of requests makeManyRequests(makeUncheckedRequest, 50, () => { // Give outlier detection time to run ejection checks setTimeout(() => { // Make enough requests to go around all servers makeManyRequests(makeCheckedRequest, 10, done); }, 1000); }); }); it('Should uneject a server after the ejection period', function (done) { this.timeout(5000); makeManyRequests(makeUncheckedRequest, 50, () => { setTimeout(() => { makeManyRequests(makeCheckedRequest, 10, error => { if (error) { done(error); return; } setTimeout(() => { makeManyRequests(makeCheckedRequest, 10, error => { assert(error); done(); }); }, 3000); }); }, 1000); }); }); }); describe('Failure percentage', () => { let makeCheckedRequest: (callback: () => void) => void; let makeUncheckedRequest: (callback: (error?: Error) => void) => void; before(() => { const target = 'ipv4:///' + goodPorts.map(port => `127.0.0.1:${port}`).join(',') + `,127.0.0.1:${badPort}`; const client = new EchoService( target, grpc.credentials.createInsecure(), { 'grpc.service_config': falurePercentageOutlierDetectionServiceConfigString, } ); makeUncheckedRequest = (callback: () => void) => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { callback(); } ); }; makeCheckedRequest = (callback: (error?: Error) => void) => { client.echo( { value: 'test value', value2: 3 }, (error: grpc.ServiceError, response: any) => { callback(error); } ); }; }); it('Should eject a server if it is failing requests', done => { // Make a large volume of requests makeManyRequests(makeUncheckedRequest, 50, () => { // Give outlier detection time to run ejection checks setTimeout(() => { // Make enough requests to go around all servers makeManyRequests(makeCheckedRequest, 10, done); }, 1000); }); }); it('Should uneject a server after the ejection period', function (done) { this.timeout(5000); makeManyRequests(makeUncheckedRequest, 50, () => { setTimeout(() => { makeManyRequests(makeCheckedRequest, 10, error => { if (error) { done(error); return; } setTimeout(() => { makeManyRequests(makeCheckedRequest, 10, error => { assert(error); done(); }); }, 3000); }); }, 1000); }); }); }); });