mirror of https://github.com/grpc/grpc-node.git
grpc-health-check: Implement version 2.0 update
This commit is contained in:
parent
afbdbdeec3
commit
524bb7d341
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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');
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue