grpc-health-check: Implement version 2.0 update

This commit is contained in:
Michael Lumish 2023-09-18 14:55:16 -07:00
parent afbdbdeec3
commit 524bb7d341
11 changed files with 599 additions and 192 deletions

View File

@ -4,11 +4,7 @@ Health check client and service for use with gRPC-node.
## Background
This package exports both a client and server that adhere to the [gRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md).
By using this package, clients and servers can rely on common proto and service definitions. This means:
- Clients can use the generated stubs to health check _any_ server that adheres to the protocol.
- Servers do not reimplement common logic for publishing health statuses.
This package provides an implementation of the [gRPC Health Checking Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) service, as described in [gRFC L106](https://github.com/grpc/proposal/blob/master/L106-node-heath-check-library.md).
## Installation
@ -22,33 +18,39 @@ npm install grpc-health-check
### Server
Any gRPC-node server can use `grpc-health-check` to adhere to the gRPC Health Checking Protocol.
Any gRPC-node server can use `grpc-health-check` to adhere to the gRPC Health Checking Protocol.
The following shows how this package can be added to a pre-existing gRPC server.
```javascript 1.8
```typescript
// Import package
let health = require('grpc-health-check');
import { HealthImplementation, ServingStatusMap } from 'grpc-health-check';
// Define service status map. Key is the service name, value is the corresponding status.
// By convention, the empty string "" key represents that status of the entire server.
// By convention, the empty string '' key represents that status of the entire server.
const statusMap = {
"ServiceFoo": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.SERVING,
"ServiceBar": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING,
"": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING,
'ServiceFoo': 'SERVING',
'ServiceBar': 'NOT_SERVING',
'': 'NOT_SERVING',
};
// Construct the service implementation
let healthImpl = new health.Implementation(statusMap);
const healthImpl = new HealthImplementation(statusMap);
// Add the service and implementation to your pre-existing gRPC-node server
server.addService(health.service, healthImpl);
healthImpl.addToServer(server);
// When ServiceBar comes up
healthImpl.setStatus('serviceBar', 'SERVING');
```
Congrats! Your server now allows any client to run a health check against it.
### Client
Any gRPC-node client can use `grpc-health-check` to run health checks against other servers that follow the protocol.
Any gRPC-node client can use the `service` object exported by `grpc-health-check` to generate clients that can make health check requests.
### Command Line Usage
The absolute path to `health.proto` can be obtained on the command line with `node -p 'require("grpc-health-check").protoPath'`.
## Contributing

View File

@ -19,22 +19,32 @@ import * as gulp from 'gulp';
import * as mocha from 'gulp-mocha';
import * as execa from 'execa';
import * as path from 'path';
import * as del from 'del';
import {linkSync} from '../../util';
const healthCheckDir = __dirname;
const baseDir = path.resolve(healthCheckDir, '..', '..');
const testDir = path.resolve(healthCheckDir, 'test');
const outDir = path.resolve(healthCheckDir, 'build');
const runInstall = () => execa('npm', ['install', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'});
const execNpmVerb = (verb: string, ...args: string[]) =>
execa('npm', [verb, ...args], {cwd: healthCheckDir, stdio: 'inherit'});
const execNpmCommand = execNpmVerb.bind(null, 'run');
const runRebuild = () => execa('npm', ['rebuild', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'});
const install = () => execNpmVerb('install', '--unsafe-perm');
const install = gulp.series(runInstall, runRebuild);
/**
* Transpiles TypeScript files in src/ to JavaScript according to the settings
* found in tsconfig.json.
*/
const compile = () => execNpmCommand('compile');
const test = () => gulp.src(`${testDir}/*.js`).pipe(mocha({reporter: 'mocha-jenkins-reporter'}));
const runTests = () => {
return gulp.src(`${outDir}/test/**/*.js`)
.pipe(mocha({reporter: 'mocha-jenkins-reporter',
require: ['ts-node/register']}));
};
const test = gulp.series(install, runTests);
export {
install,
compile,
test
}
}

View File

@ -1,55 +0,0 @@
/*
*
* Copyright 2015 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.
*
*/
'use strict';
var grpc = require('grpc');
var _get = require('lodash.get');
var _clone = require('lodash.clone')
var health_messages = require('./v1/health_pb');
var health_service = require('./v1/health_grpc_pb');
function HealthImplementation(statusMap) {
this.statusMap = _clone(statusMap);
}
HealthImplementation.prototype.setStatus = function(service, status) {
this.statusMap[service] = status;
};
HealthImplementation.prototype.check = function(call, callback){
var service = call.request.getService();
var status = _get(this.statusMap, service, null);
if (status === null) {
// TODO(murgatroid99): Do this without an explicit reference to grpc.
callback({code:grpc.status.NOT_FOUND});
} else {
var response = new health_messages.HealthCheckResponse();
response.setStatus(status);
callback(null, response);
}
};
module.exports = {
Client: health_service.HealthClient,
messages: health_messages,
service: health_service.HealthService,
Implementation: HealthImplementation
};

View File

@ -1,6 +1,6 @@
{
"name": "grpc-health-check",
"version": "1.8.0",
"version": "2.0.0",
"author": "Google Inc.",
"description": "Health check client and service for use with gRPC-node",
"repository": {
@ -14,18 +14,27 @@
"email": "mlumish@google.com"
}
],
"scripts": {
"compile": "tsc -p .",
"prepare": "npm run generate-types && npm run compile",
"generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ -O src/generated health/v1/health.proto",
"generate-test-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ -O test/generated --grpcLib=@grpc/grpc-js health/v1/health.proto"
},
"dependencies": {
"google-protobuf": "^3.4.0",
"grpc": "^1.6.0",
"lodash.clone": "^4.5.0",
"lodash.get": "^4.4.2"
"@grpc/proto-loader": "^0.7.10",
"typescript": "^5.2.2"
},
"files": [
"LICENSE",
"README.md",
"health.js",
"v1"
"src",
"build",
"proto"
],
"main": "health.js",
"license": "Apache-2.0"
"main": "build/src/health.js",
"types": "build/src/health.d.ts",
"license": "Apache-2.0",
"devDependencies": {
"@grpc/grpc-js": "file:../grpc-js"
}
}

View File

@ -0,0 +1,73 @@
// Copyright 2015 The 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.
// The canonical version of this proto can be found at
// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto
syntax = "proto3";
package grpc.health.v1;
option csharp_namespace = "Grpc.Health.V1";
option go_package = "google.golang.org/grpc/health/grpc_health_v1";
option java_multiple_files = true;
option java_outer_classname = "HealthProto";
option java_package = "io.grpc.health.v1";
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
}
ServingStatus status = 1;
}
// Health is gRPC's mechanism for checking whether a server is able to handle
// RPCs. Its semantics are documented in
// https://github.com/grpc/grpc/blob/master/doc/health-checking.md.
service Health {
// Check gets the health of the specified service. If the requested service
// is unknown, the call will fail with status NOT_FOUND. If the caller does
// not specify a service name, the server should respond with its overall
// health status.
//
// Clients should set a deadline when calling Check, and can declare the
// server unhealthy if they do not receive a timely response.
//
// Check implementations should be idempotent and side effect free.
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
// Performs a watch for the serving status of the requested service.
// The server will immediately send back a message indicating the current
// serving status. It will then subsequently send a new message whenever
// the service's serving status changes.
//
// If the requested service is unknown when the call is received, the
// server will send a message setting the serving status to
// SERVICE_UNKNOWN but will *not* terminate the call. If at some
// future point, the serving status of the service becomes known, the
// server will send a new message with the service's serving status.
//
// If the call terminates with status UNIMPLEMENTED, then clients
// should assume this method is not supported and should not retry the
// call. If the call terminates with any other status (including OK),
// clients should retry the call with appropriate exponential backoff.
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

View File

@ -0,0 +1,112 @@
/*
*
* Copyright 2023 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 path from 'path';
import { loadSync, ServiceDefinition } from '@grpc/proto-loader';
import { HealthCheckRequest__Output } from './generated/grpc/health/v1/HealthCheckRequest';
import { HealthCheckResponse } from './generated/grpc/health/v1/HealthCheckResponse';
import { sendUnaryData, Server, ServerUnaryCall, ServerWritableStream } from './server-type';
const loadedProto = loadSync('health/v1/health.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
includeDirs: [`${__dirname}/../../proto`],
});
export const service = loadedProto['grpc.health.v1.Health'] as ServiceDefinition;
const GRPC_STATUS_NOT_FOUND = 5;
export type ServingStatus = 'UNKNOWN' | 'SERVING' | 'NOT_SERVING';
export interface ServingStatusMap {
[serviceName: string]: ServingStatus;
}
interface StatusWatcher {
(status: ServingStatus): void;
}
export class HealthImplementation {
private statusMap: Map<string, ServingStatus> = new Map();
private watchers: Map<string, Set<StatusWatcher>> = new Map();
constructor(initialStatusMap?: ServingStatusMap) {
if (initialStatusMap) {
for (const [serviceName, status] of Object.entries(initialStatusMap)) {
this.statusMap.set(serviceName, status);
}
}
}
setStatus(service: string, status: ServingStatus) {
this.statusMap.set(service, status);
for (const watcher of this.watchers.get(service) ?? []) {
watcher(status);
}
}
private addWatcher(service: string, watcher: StatusWatcher) {
const existingWatcherSet = this.watchers.get(service);
if (existingWatcherSet) {
existingWatcherSet.add(watcher);
} else {
const newWatcherSet = new Set<StatusWatcher>();
newWatcherSet.add(watcher);
this.watchers.set(service, newWatcherSet);
}
}
private removeWatcher(service: string, watcher: StatusWatcher) {
this.watchers.get(service)?.delete(watcher);
}
addToServer(server: Server) {
server.addService(service, {
check: (call: ServerUnaryCall<HealthCheckRequest__Output, HealthCheckResponse>, callback: sendUnaryData<HealthCheckResponse>) => {
const serviceName = call.request.service;
const status = this.statusMap.get(serviceName);
if (status) {
callback(null, {status: status});
} else {
callback({code: GRPC_STATUS_NOT_FOUND, details: `Health status unknown for service ${serviceName}`});
}
},
watch: (call: ServerWritableStream<HealthCheckRequest__Output, HealthCheckResponse>) => {
const serviceName = call.request.service;
const statusWatcher = (status: ServingStatus) => {
call.write({status: status});
};
this.addWatcher(serviceName, statusWatcher);
call.on('cancelled', () => {
this.removeWatcher(serviceName, statusWatcher);
});
const currentStatus = this.statusMap.get(serviceName);
if (currentStatus) {
call.write({status: currentStatus});
} else {
call.write({status: 'SERVICE_UNKNOWN'});
}
}
});
}
}
export const protoPath = path.resolve(__dirname, '../../proto/health/v1/health.proto');

View File

@ -0,0 +1,75 @@
/*
* Copyright 2023 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 { Readable, Writable } from 'stream';
interface EmitterAugmentation1<Name extends string | symbol, Arg> {
addListener(event: Name, listener: (arg1: Arg) => void): this;
emit(event: Name, arg1: Arg): boolean;
on(event: Name, listener: (arg1: Arg) => void): this;
once(event: Name, listener: (arg1: Arg) => void): this;
prependListener(event: Name, listener: (arg1: Arg) => void): this;
prependOnceListener(event: Name, listener: (arg1: Arg) => void): this;
removeListener(event: Name, listener: (arg1: Arg) => void): this;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export type WriteCallback = (error: Error | null | undefined) => void;
export interface IntermediateObjectReadable<T> extends Readable {
read(size?: number): any & T;
}
export type ObjectReadable<T> = {
read(size?: number): T;
} & EmitterAugmentation1<'data', T> &
IntermediateObjectReadable<T>;
export interface IntermediateObjectWritable<T> extends Writable {
_write(chunk: any & T, encoding: string, callback: Function): void;
write(chunk: any & T, cb?: WriteCallback): boolean;
write(chunk: any & T, encoding?: any, cb?: WriteCallback): boolean;
setDefaultEncoding(encoding: string): this;
end(): ReturnType<Writable['end']> extends Writable ? this : void;
end(
chunk: any & T,
cb?: Function
): ReturnType<Writable['end']> extends Writable ? this : void;
end(
chunk: any & T,
encoding?: any,
cb?: Function
): ReturnType<Writable['end']> extends Writable ? this : void;
}
export interface ObjectWritable<T> extends IntermediateObjectWritable<T> {
_write(chunk: T, encoding: string, callback: Function): void;
write(chunk: T, cb?: Function): boolean;
write(chunk: T, encoding?: any, cb?: Function): boolean;
setDefaultEncoding(encoding: string): this;
end(): ReturnType<Writable['end']> extends Writable ? this : void;
end(
chunk: T,
cb?: Function
): ReturnType<Writable['end']> extends Writable ? this : void;
end(
chunk: T,
encoding?: any,
cb?: Function
): ReturnType<Writable['end']> extends Writable ? this : void;
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 2023 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 { ServiceDefinition } from '@grpc/proto-loader';
import { ObjectReadable, ObjectWritable } from './object-stream';
import { EventEmitter } from 'events';
type Metadata = any;
interface StatusObject {
code: number;
details: string;
metadata: Metadata;
}
type Deadline = Date | number;
type ServerStatusResponse = Partial<StatusObject>;
type ServerErrorResponse = ServerStatusResponse & Error;
type ServerSurfaceCall = {
cancelled: boolean;
readonly metadata: Metadata;
getPeer(): string;
sendMetadata(responseMetadata: Metadata): void;
getDeadline(): Deadline;
getPath(): string;
} & EventEmitter;
export type ServerUnaryCall<RequestType, ResponseType> = ServerSurfaceCall & {
request: RequestType;
};
type ServerReadableStream<RequestType, ResponseType> =
ServerSurfaceCall & ObjectReadable<RequestType>;
export type ServerWritableStream<RequestType, ResponseType> =
ServerSurfaceCall &
ObjectWritable<ResponseType> & {
request: RequestType;
end: (metadata?: Metadata) => void;
};
type ServerDuplexStream<RequestType, ResponseType> = ServerSurfaceCall &
ObjectReadable<RequestType> &
ObjectWritable<ResponseType> & { end: (metadata?: Metadata) => void };
// Unary response callback signature.
export type sendUnaryData<ResponseType> = (
error: ServerErrorResponse | ServerStatusResponse | null,
value?: ResponseType | null,
trailer?: Metadata,
flags?: number
) => void;
// User provided handler for unary calls.
type handleUnaryCall<RequestType, ResponseType> = (
call: ServerUnaryCall<RequestType, ResponseType>,
callback: sendUnaryData<ResponseType>
) => void;
// User provided handler for client streaming calls.
type handleClientStreamingCall<RequestType, ResponseType> = (
call: ServerReadableStream<RequestType, ResponseType>,
callback: sendUnaryData<ResponseType>
) => void;
// User provided handler for server streaming calls.
type handleServerStreamingCall<RequestType, ResponseType> = (
call: ServerWritableStream<RequestType, ResponseType>
) => void;
// User provided handler for bidirectional streaming calls.
type handleBidiStreamingCall<RequestType, ResponseType> = (
call: ServerDuplexStream<RequestType, ResponseType>
) => void;
export type HandleCall<RequestType, ResponseType> =
| handleUnaryCall<RequestType, ResponseType>
| handleClientStreamingCall<RequestType, ResponseType>
| handleServerStreamingCall<RequestType, ResponseType>
| handleBidiStreamingCall<RequestType, ResponseType>;
export type UntypedHandleCall = HandleCall<any, any>;
export interface UntypedServiceImplementation {
[name: string]: UntypedHandleCall;
}
export interface Server {
addService(service: ServiceDefinition, implementation: UntypedServiceImplementation): void;
}

View File

@ -1,103 +0,0 @@
/*
*
* Copyright 2015 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.
*
*/
'use strict';
var assert = require('assert');
var health = require('../health');
var health_messages = require('../v1/health_pb');
var ServingStatus = health_messages.HealthCheckResponse.ServingStatus;
var grpc = require('grpc');
describe('Health Checking', function() {
var statusMap = {
'': ServingStatus.SERVING,
'grpc.test.TestServiceNotServing': ServingStatus.NOT_SERVING,
'grpc.test.TestServiceServing': ServingStatus.SERVING
};
var healthServer;
var healthImpl;
var healthClient;
before(function() {
healthServer = new grpc.Server();
healthImpl = new health.Implementation(statusMap);
healthServer.addService(health.service, healthImpl);
var port_num = healthServer.bind('0.0.0.0:0',
grpc.ServerCredentials.createInsecure());
healthServer.start();
healthClient = new health.Client('localhost:' + port_num,
grpc.credentials.createInsecure());
});
after(function() {
healthServer.forceShutdown();
});
it('should say an enabled service is SERVING', function(done) {
var request = new health_messages.HealthCheckRequest();
request.setService('');
healthClient.check(request, function(err, response) {
assert.ifError(err);
assert.strictEqual(response.getStatus(), ServingStatus.SERVING);
done();
});
});
it('should say that a disabled service is NOT_SERVING', function(done) {
var request = new health_messages.HealthCheckRequest();
request.setService('grpc.test.TestServiceNotServing');
healthClient.check(request, function(err, response) {
assert.ifError(err);
assert.strictEqual(response.getStatus(), ServingStatus.NOT_SERVING);
done();
});
});
it('should say that an enabled service is SERVING', function(done) {
var request = new health_messages.HealthCheckRequest();
request.setService('grpc.test.TestServiceServing');
healthClient.check(request, function(err, response) {
assert.ifError(err);
assert.strictEqual(response.getStatus(), ServingStatus.SERVING);
done();
});
});
it('should get NOT_FOUND if the service is not registered', function(done) {
var request = new health_messages.HealthCheckRequest();
request.setService('not_registered');
healthClient.check(request, function(err, response) {
assert(err);
assert.strictEqual(err.code, grpc.status.NOT_FOUND);
done();
});
});
it('should get a different response if the status changes', function(done) {
var request = new health_messages.HealthCheckRequest();
request.setService('transient');
healthClient.check(request, function(err, response) {
assert(err);
assert.strictEqual(err.code, grpc.status.NOT_FOUND);
healthImpl.setStatus('transient', ServingStatus.SERVING);
healthClient.check(request, function(err, response) {
assert.ifError(err);
assert.strictEqual(response.getStatus(), ServingStatus.SERVING);
done();
});
});
});
});

View File

@ -0,0 +1,152 @@
/*
*
* Copyright 2023 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 grpc from '@grpc/grpc-js';
import { HealthImplementation, ServingStatusMap, service as healthServiceDefinition } from '../src/health';
import { HealthClient } from './generated/grpc/health/v1/Health';
import { HealthCheckResponse__Output, _grpc_health_v1_HealthCheckResponse_ServingStatus__Output } from './generated/grpc/health/v1/HealthCheckResponse';
describe('Health checking', () => {
const statusMap: ServingStatusMap = {
'': 'SERVING',
'grpc.test.TestServiceNotServing': 'NOT_SERVING',
'grpc.test.TestServiceServing': 'SERVING'
};
let healthServer: grpc.Server;
let healthClient: HealthClient;
let healthImpl: HealthImplementation;
beforeEach(done => {
healthServer = new grpc.Server();
healthImpl = new HealthImplementation(statusMap);
healthImpl.addToServer(healthServer);
healthServer.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (error, port) => {
if (error) {
done(error);
return;
}
const HealthClientConstructor = grpc.makeClientConstructor(healthServiceDefinition, 'grpc.health.v1.HealthService');
healthClient = new HealthClientConstructor(`localhost:${port}`, grpc.credentials.createInsecure()) as unknown as HealthClient;
healthServer.start();
done();
});
});
afterEach((done) => {
healthClient.close();
healthServer.tryShutdown(done);
});
describe('check', () => {
it('Should say that an enabled service is SERVING', done => {
healthClient.check({service: ''}, (error, value) => {
assert.ifError(error);
assert.strictEqual(value?.status, 'SERVING');
done();
});
});
it('Should say that a disabled service is NOT_SERVING', done => {
healthClient.check({service: 'grpc.test.TestServiceNotServing'}, (error, value) => {
assert.ifError(error);
assert.strictEqual(value?.status, 'NOT_SERVING');
done();
});
});
it('Should get NOT_FOUND if the service is not registered', done => {
healthClient.check({service: 'not_registered'}, (error, value) => {
assert(error);
assert.strictEqual(error.code, grpc.status.NOT_FOUND);
done();
});
});
it('Should get a different response if the health status changes', done => {
healthClient.check({service: 'transient'}, (error, value) => {
assert(error);
assert.strictEqual(error.code, grpc.status.NOT_FOUND);
healthImpl.setStatus('transient', 'SERVING');
healthClient.check({service: 'transient'}, (error, value) => {
assert.ifError(error);
assert.strictEqual(value?.status, 'SERVING');
done();
});
});
});
});
describe('watch', () => {
it('Should respond with the health status for an existing service', done => {
const call = healthClient.watch({service: ''});
call.on('data', (response: HealthCheckResponse__Output) => {
assert.strictEqual(response.status, 'SERVING');
call.cancel();
});
call.on('error', () => {});
call.on('status', status => {
assert.strictEqual(status.code, grpc.status.CANCELLED);
done();
});
});
it('Should send a new update when the status changes', done => {
const receivedStatusList: _grpc_health_v1_HealthCheckResponse_ServingStatus__Output[] = [];
const call = healthClient.watch({service: 'grpc.test.TestServiceServing'});
call.on('data', (response: HealthCheckResponse__Output) => {
switch (receivedStatusList.length) {
case 0:
assert.strictEqual(response.status, 'SERVING');
healthImpl.setStatus('grpc.test.TestServiceServing', 'NOT_SERVING');
break;
case 1:
assert.strictEqual(response.status, 'NOT_SERVING');
call.cancel();
break;
default:
assert.fail(`Unexpected third status update ${response.status}`);
}
receivedStatusList.push(response.status);
});
call.on('error', () => {});
call.on('status', status => {
assert.deepStrictEqual(receivedStatusList, ['SERVING', 'NOT_SERVING']);
assert.strictEqual(status.code, grpc.status.CANCELLED);
done();
});
});
it('Should update when a service that did not exist is added', done => {
const receivedStatusList: _grpc_health_v1_HealthCheckResponse_ServingStatus__Output[] = [];
const call = healthClient.watch({service: 'transient'});
call.on('data', (response: HealthCheckResponse__Output) => {
switch (receivedStatusList.length) {
case 0:
assert.strictEqual(response.status, 'SERVICE_UNKNOWN');
healthImpl.setStatus('transient', 'SERVING');
break;
case 1:
assert.strictEqual(response.status, 'SERVING');
call.cancel();
break;
default:
assert.fail(`Unexpected third status update ${response.status}`);
}
receivedStatusList.push(response.status);
});
call.on('error', () => {});
call.on('status', status => {
assert.deepStrictEqual(receivedStatusList, ['SERVICE_UNKNOWN', 'SERVING']);
assert.strictEqual(status.code, grpc.status.CANCELLED);
done();
});
})
});
});

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"pretty": true,
"sourceMap": true,
"strict": true,
"lib": ["es2017"],
"outDir": "build",
"target": "es2017",
"module": "commonjs",
"resolveJsonModule": true,
"incremental": true,
"types": ["mocha"],
"noUnusedLocals": true
},
"include": [
"src/**/*.ts",
"test/**/*.ts"
],
"exclude": [
"node_modules"
]
}