Skip to content

Commit 524bb7d

Browse files
committed
grpc-health-check: Implement version 2.0 update
1 parent afbdbde commit 524bb7d

File tree

11 files changed

+599
-192
lines changed

11 files changed

+599
-192
lines changed

packages/grpc-health-check/README.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ Health check client and service for use with gRPC-node.
44

55
## Background
66

7-
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).
8-
9-
By using this package, clients and servers can rely on common proto and service definitions. This means:
10-
- Clients can use the generated stubs to health check _any_ server that adheres to the protocol.
11-
- Servers do not reimplement common logic for publishing health statuses.
7+
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).
128

139
## Installation
1410

@@ -22,33 +18,39 @@ npm install grpc-health-check
2218

2319
### Server
2420

25-
Any gRPC-node server can use `grpc-health-check` to adhere to the gRPC Health Checking Protocol.
21+
Any gRPC-node server can use `grpc-health-check` to adhere to the gRPC Health Checking Protocol.
2622
The following shows how this package can be added to a pre-existing gRPC server.
2723

28-
```javascript 1.8
24+
```typescript
2925
// Import package
30-
let health = require('grpc-health-check');
26+
import { HealthImplementation, ServingStatusMap } from 'grpc-health-check';
3127

3228
// Define service status map. Key is the service name, value is the corresponding status.
33-
// By convention, the empty string "" key represents that status of the entire server.
29+
// By convention, the empty string '' key represents that status of the entire server.
3430
const statusMap = {
35-
"ServiceFoo": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.SERVING,
36-
"ServiceBar": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING,
37-
"": proto.grpc.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING,
31+
'ServiceFoo': 'SERVING',
32+
'ServiceBar': 'NOT_SERVING',
33+
'': 'NOT_SERVING',
3834
};
3935

4036
// Construct the service implementation
41-
let healthImpl = new health.Implementation(statusMap);
37+
const healthImpl = new HealthImplementation(statusMap);
38+
39+
healthImpl.addToServer(server);
4240

43-
// Add the service and implementation to your pre-existing gRPC-node server
44-
server.addService(health.service, healthImpl);
41+
// When ServiceBar comes up
42+
healthImpl.setStatus('serviceBar', 'SERVING');
4543
```
4644

4745
Congrats! Your server now allows any client to run a health check against it.
4846

4947
### Client
5048

51-
Any gRPC-node client can use `grpc-health-check` to run health checks against other servers that follow the protocol.
49+
Any gRPC-node client can use the `service` object exported by `grpc-health-check` to generate clients that can make health check requests.
50+
51+
### Command Line Usage
52+
53+
The absolute path to `health.proto` can be obtained on the command line with `node -p 'require("grpc-health-check").protoPath'`.
5254

5355
## Contributing
5456

packages/grpc-health-check/gulpfile.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,32 @@ import * as gulp from 'gulp';
1919
import * as mocha from 'gulp-mocha';
2020
import * as execa from 'execa';
2121
import * as path from 'path';
22-
import * as del from 'del';
23-
import {linkSync} from '../../util';
2422

2523
const healthCheckDir = __dirname;
26-
const baseDir = path.resolve(healthCheckDir, '..', '..');
27-
const testDir = path.resolve(healthCheckDir, 'test');
24+
const outDir = path.resolve(healthCheckDir, 'build');
2825

29-
const runInstall = () => execa('npm', ['install', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'});
26+
const execNpmVerb = (verb: string, ...args: string[]) =>
27+
execa('npm', [verb, ...args], {cwd: healthCheckDir, stdio: 'inherit'});
28+
const execNpmCommand = execNpmVerb.bind(null, 'run');
3029

31-
const runRebuild = () => execa('npm', ['rebuild', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'});
30+
const install = () => execNpmVerb('install', '--unsafe-perm');
3231

33-
const install = gulp.series(runInstall, runRebuild);
32+
/**
33+
* Transpiles TypeScript files in src/ to JavaScript according to the settings
34+
* found in tsconfig.json.
35+
*/
36+
const compile = () => execNpmCommand('compile');
37+
38+
const runTests = () => {
39+
return gulp.src(`${outDir}/test/**/*.js`)
40+
.pipe(mocha({reporter: 'mocha-jenkins-reporter',
41+
require: ['ts-node/register']}));
42+
};
3443

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

3746
export {
3847
install,
48+
compile,
3949
test
40-
}
50+
}

packages/grpc-health-check/health.js

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "grpc-health-check",
3-
"version": "1.8.0",
3+
"version": "2.0.0",
44
"author": "Google Inc.",
55
"description": "Health check client and service for use with gRPC-node",
66
"repository": {
@@ -14,18 +14,27 @@
1414
"email": "[email protected]"
1515
}
1616
],
17+
"scripts": {
18+
"compile": "tsc -p .",
19+
"prepare": "npm run generate-types && npm run compile",
20+
"generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ -O src/generated health/v1/health.proto",
21+
"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"
22+
},
1723
"dependencies": {
18-
"google-protobuf": "^3.4.0",
19-
"grpc": "^1.6.0",
20-
"lodash.clone": "^4.5.0",
21-
"lodash.get": "^4.4.2"
24+
"@grpc/proto-loader": "^0.7.10",
25+
"typescript": "^5.2.2"
2226
},
2327
"files": [
2428
"LICENSE",
2529
"README.md",
26-
"health.js",
27-
"v1"
30+
"src",
31+
"build",
32+
"proto"
2833
],
29-
"main": "health.js",
30-
"license": "Apache-2.0"
34+
"main": "build/src/health.js",
35+
"types": "build/src/health.d.ts",
36+
"license": "Apache-2.0",
37+
"devDependencies": {
38+
"@grpc/grpc-js": "file:../grpc-js"
39+
}
3140
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2015 The gRPC Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// The canonical version of this proto can be found at
16+
// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto
17+
18+
syntax = "proto3";
19+
20+
package grpc.health.v1;
21+
22+
option csharp_namespace = "Grpc.Health.V1";
23+
option go_package = "google.golang.org/grpc/health/grpc_health_v1";
24+
option java_multiple_files = true;
25+
option java_outer_classname = "HealthProto";
26+
option java_package = "io.grpc.health.v1";
27+
28+
message HealthCheckRequest {
29+
string service = 1;
30+
}
31+
32+
message HealthCheckResponse {
33+
enum ServingStatus {
34+
UNKNOWN = 0;
35+
SERVING = 1;
36+
NOT_SERVING = 2;
37+
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
38+
}
39+
ServingStatus status = 1;
40+
}
41+
42+
// Health is gRPC's mechanism for checking whether a server is able to handle
43+
// RPCs. Its semantics are documented in
44+
// https://github.com/grpc/grpc/blob/master/doc/health-checking.md.
45+
service Health {
46+
// Check gets the health of the specified service. If the requested service
47+
// is unknown, the call will fail with status NOT_FOUND. If the caller does
48+
// not specify a service name, the server should respond with its overall
49+
// health status.
50+
//
51+
// Clients should set a deadline when calling Check, and can declare the
52+
// server unhealthy if they do not receive a timely response.
53+
//
54+
// Check implementations should be idempotent and side effect free.
55+
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
56+
57+
// Performs a watch for the serving status of the requested service.
58+
// The server will immediately send back a message indicating the current
59+
// serving status. It will then subsequently send a new message whenever
60+
// the service's serving status changes.
61+
//
62+
// If the requested service is unknown when the call is received, the
63+
// server will send a message setting the serving status to
64+
// SERVICE_UNKNOWN but will *not* terminate the call. If at some
65+
// future point, the serving status of the service becomes known, the
66+
// server will send a new message with the service's serving status.
67+
//
68+
// If the call terminates with status UNIMPLEMENTED, then clients
69+
// should assume this method is not supported and should not retry the
70+
// call. If the call terminates with any other status (including OK),
71+
// clients should retry the call with appropriate exponential backoff.
72+
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
73+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
*
3+
* Copyright 2023 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
import * as path from 'path';
20+
import { loadSync, ServiceDefinition } from '@grpc/proto-loader';
21+
import { HealthCheckRequest__Output } from './generated/grpc/health/v1/HealthCheckRequest';
22+
import { HealthCheckResponse } from './generated/grpc/health/v1/HealthCheckResponse';
23+
import { sendUnaryData, Server, ServerUnaryCall, ServerWritableStream } from './server-type';
24+
25+
const loadedProto = loadSync('health/v1/health.proto', {
26+
keepCase: true,
27+
longs: String,
28+
enums: String,
29+
defaults: true,
30+
oneofs: true,
31+
includeDirs: [`${__dirname}/../../proto`],
32+
});
33+
34+
export const service = loadedProto['grpc.health.v1.Health'] as ServiceDefinition;
35+
36+
const GRPC_STATUS_NOT_FOUND = 5;
37+
38+
export type ServingStatus = 'UNKNOWN' | 'SERVING' | 'NOT_SERVING';
39+
40+
export interface ServingStatusMap {
41+
[serviceName: string]: ServingStatus;
42+
}
43+
44+
interface StatusWatcher {
45+
(status: ServingStatus): void;
46+
}
47+
48+
export class HealthImplementation {
49+
private statusMap: Map<string, ServingStatus> = new Map();
50+
private watchers: Map<string, Set<StatusWatcher>> = new Map();
51+
constructor(initialStatusMap?: ServingStatusMap) {
52+
if (initialStatusMap) {
53+
for (const [serviceName, status] of Object.entries(initialStatusMap)) {
54+
this.statusMap.set(serviceName, status);
55+
}
56+
}
57+
}
58+
59+
setStatus(service: string, status: ServingStatus) {
60+
this.statusMap.set(service, status);
61+
for (const watcher of this.watchers.get(service) ?? []) {
62+
watcher(status);
63+
}
64+
}
65+
66+
private addWatcher(service: string, watcher: StatusWatcher) {
67+
const existingWatcherSet = this.watchers.get(service);
68+
if (existingWatcherSet) {
69+
existingWatcherSet.add(watcher);
70+
} else {
71+
const newWatcherSet = new Set<StatusWatcher>();
72+
newWatcherSet.add(watcher);
73+
this.watchers.set(service, newWatcherSet);
74+
}
75+
}
76+
77+
private removeWatcher(service: string, watcher: StatusWatcher) {
78+
this.watchers.get(service)?.delete(watcher);
79+
}
80+
81+
addToServer(server: Server) {
82+
server.addService(service, {
83+
check: (call: ServerUnaryCall<HealthCheckRequest__Output, HealthCheckResponse>, callback: sendUnaryData<HealthCheckResponse>) => {
84+
const serviceName = call.request.service;
85+
const status = this.statusMap.get(serviceName);
86+
if (status) {
87+
callback(null, {status: status});
88+
} else {
89+
callback({code: GRPC_STATUS_NOT_FOUND, details: `Health status unknown for service ${serviceName}`});
90+
}
91+
},
92+
watch: (call: ServerWritableStream<HealthCheckRequest__Output, HealthCheckResponse>) => {
93+
const serviceName = call.request.service;
94+
const statusWatcher = (status: ServingStatus) => {
95+
call.write({status: status});
96+
};
97+
this.addWatcher(serviceName, statusWatcher);
98+
call.on('cancelled', () => {
99+
this.removeWatcher(serviceName, statusWatcher);
100+
});
101+
const currentStatus = this.statusMap.get(serviceName);
102+
if (currentStatus) {
103+
call.write({status: currentStatus});
104+
} else {
105+
call.write({status: 'SERVICE_UNKNOWN'});
106+
}
107+
}
108+
});
109+
}
110+
}
111+
112+
export const protoPath = path.resolve(__dirname, '../../proto/health/v1/health.proto');

0 commit comments

Comments
 (0)