Skip to content

Commit

Permalink
grpc-health-check: Implement version 2.0 update
Browse files Browse the repository at this point in the history
  • Loading branch information
murgatroid99 committed Sep 18, 2023
1 parent afbdbde commit 524bb7d
Show file tree
Hide file tree
Showing 11 changed files with 599 additions and 192 deletions.
34 changes: 18 additions & 16 deletions packages/grpc-health-check/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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);

healthImpl.addToServer(server);

// Add the service and implementation to your pre-existing gRPC-node server
server.addService(health.service, healthImpl);
// 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

Expand Down
28 changes: 19 additions & 9 deletions packages/grpc-health-check/gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 runTests = () => {
return gulp.src(`${outDir}/test/**/*.js`)
.pipe(mocha({reporter: 'mocha-jenkins-reporter',
require: ['ts-node/register']}));
};

const test = () => gulp.src(`${testDir}/*.js`).pipe(mocha({reporter: 'mocha-jenkins-reporter'}));
const test = gulp.series(install, runTests);

export {
install,
compile,
test
}
}
55 changes: 0 additions & 55 deletions packages/grpc-health-check/health.js

This file was deleted.

27 changes: 18 additions & 9 deletions packages/grpc-health-check/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -14,18 +14,27 @@
"email": "[email protected]"
}
],
"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"
}
}
73 changes: 73 additions & 0 deletions packages/grpc-health-check/proto/health/v1/health.proto
Original file line number Diff line number Diff line change
@@ -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);
}
112 changes: 112 additions & 0 deletions packages/grpc-health-check/src/health.ts
Original file line number Diff line number Diff line change
@@ -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');
Loading

0 comments on commit 524bb7d

Please sign in to comment.