From afbdbdeec38f92ebcde0e5147910172b524605a1 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 18 Sep 2023 13:50:42 -0700 Subject: [PATCH 1/2] grpc-health-check: Add generated code for version 2.0 --- .../src/generated/grpc/health/v1/Health.ts | 10 ++ .../grpc/health/v1/HealthCheckRequest.ts | 10 ++ .../grpc/health/v1/HealthCheckResponse.ts | 37 +++++ .../test/generated/grpc/health/v1/Health.ts | 129 ++++++++++++++++++ .../grpc/health/v1/HealthCheckRequest.ts | 10 ++ .../grpc/health/v1/HealthCheckResponse.ts | 37 +++++ .../test/generated/health.ts | 26 ++++ 7 files changed, 259 insertions(+) create mode 100644 packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts create mode 100644 packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts create mode 100644 packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts create mode 100644 packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts create mode 100644 packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts create mode 100644 packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts create mode 100644 packages/grpc-health-check/test/generated/health.ts diff --git a/packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts b/packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts new file mode 100644 index 000000000..a308498f4 --- /dev/null +++ b/packages/grpc-health-check/src/generated/grpc/health/v1/Health.ts @@ -0,0 +1,10 @@ +// Original file: proto/health/v1/health.proto + +import type { MethodDefinition } from '@grpc/proto-loader' +import type { HealthCheckRequest as _grpc_health_v1_HealthCheckRequest, HealthCheckRequest__Output as _grpc_health_v1_HealthCheckRequest__Output } from '../../../grpc/health/v1/HealthCheckRequest'; +import type { HealthCheckResponse as _grpc_health_v1_HealthCheckResponse, HealthCheckResponse__Output as _grpc_health_v1_HealthCheckResponse__Output } from '../../../grpc/health/v1/HealthCheckResponse'; + +export interface HealthDefinition { + Check: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> + Watch: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> +} diff --git a/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts new file mode 100644 index 000000000..71ae9df4e --- /dev/null +++ b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckRequest.ts @@ -0,0 +1,10 @@ +// Original file: proto/health/v1/health.proto + + +export interface HealthCheckRequest { + 'service'?: (string); +} + +export interface HealthCheckRequest__Output { + 'service': (string); +} diff --git a/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts new file mode 100644 index 000000000..ee4f375ae --- /dev/null +++ b/packages/grpc-health-check/src/generated/grpc/health/v1/HealthCheckResponse.ts @@ -0,0 +1,37 @@ +// Original file: proto/health/v1/health.proto + + +// Original file: proto/health/v1/health.proto + +export const _grpc_health_v1_HealthCheckResponse_ServingStatus = { + UNKNOWN: 'UNKNOWN', + SERVING: 'SERVING', + NOT_SERVING: 'NOT_SERVING', + /** + * Used only by the Watch method. + */ + SERVICE_UNKNOWN: 'SERVICE_UNKNOWN', +} as const; + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus = + | 'UNKNOWN' + | 0 + | 'SERVING' + | 1 + | 'NOT_SERVING' + | 2 + /** + * Used only by the Watch method. + */ + | 'SERVICE_UNKNOWN' + | 3 + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus__Output = typeof _grpc_health_v1_HealthCheckResponse_ServingStatus[keyof typeof _grpc_health_v1_HealthCheckResponse_ServingStatus] + +export interface HealthCheckResponse { + 'status'?: (_grpc_health_v1_HealthCheckResponse_ServingStatus); +} + +export interface HealthCheckResponse__Output { + 'status': (_grpc_health_v1_HealthCheckResponse_ServingStatus__Output); +} diff --git a/packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts b/packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts new file mode 100644 index 000000000..320958e3c --- /dev/null +++ b/packages/grpc-health-check/test/generated/grpc/health/v1/Health.ts @@ -0,0 +1,129 @@ +// Original file: proto/health/v1/health.proto + +import type * as grpc from '@grpc/grpc-js' +import type { MethodDefinition } from '@grpc/proto-loader' +import type { HealthCheckRequest as _grpc_health_v1_HealthCheckRequest, HealthCheckRequest__Output as _grpc_health_v1_HealthCheckRequest__Output } from '../../../grpc/health/v1/HealthCheckRequest'; +import type { HealthCheckResponse as _grpc_health_v1_HealthCheckResponse, HealthCheckResponse__Output as _grpc_health_v1_HealthCheckResponse__Output } from '../../../grpc/health/v1/HealthCheckResponse'; + +/** + * 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. + */ +export interface HealthClient extends grpc.Client { + /** + * 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. + */ + Check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + Check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + Check(argument: _grpc_health_v1_HealthCheckRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + Check(argument: _grpc_health_v1_HealthCheckRequest, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + /** + * 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. + */ + check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + check(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + check(argument: _grpc_health_v1_HealthCheckRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + check(argument: _grpc_health_v1_HealthCheckRequest, callback: grpc.requestCallback<_grpc_health_v1_HealthCheckResponse__Output>): grpc.ClientUnaryCall; + + /** + * 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. + */ + Watch(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + Watch(argument: _grpc_health_v1_HealthCheckRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + /** + * 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. + */ + watch(argument: _grpc_health_v1_HealthCheckRequest, metadata: grpc.Metadata, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + watch(argument: _grpc_health_v1_HealthCheckRequest, options?: grpc.CallOptions): grpc.ClientReadableStream<_grpc_health_v1_HealthCheckResponse__Output>; + +} + +/** + * 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. + */ +export interface HealthHandlers extends grpc.UntypedServiceImplementation { + /** + * 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. + */ + Check: grpc.handleUnaryCall<_grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_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. + */ + Watch: grpc.handleServerStreamingCall<_grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse>; + +} + +export interface HealthDefinition extends grpc.ServiceDefinition { + Check: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> + Watch: MethodDefinition<_grpc_health_v1_HealthCheckRequest, _grpc_health_v1_HealthCheckResponse, _grpc_health_v1_HealthCheckRequest__Output, _grpc_health_v1_HealthCheckResponse__Output> +} diff --git a/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts new file mode 100644 index 000000000..71ae9df4e --- /dev/null +++ b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckRequest.ts @@ -0,0 +1,10 @@ +// Original file: proto/health/v1/health.proto + + +export interface HealthCheckRequest { + 'service'?: (string); +} + +export interface HealthCheckRequest__Output { + 'service': (string); +} diff --git a/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts new file mode 100644 index 000000000..ee4f375ae --- /dev/null +++ b/packages/grpc-health-check/test/generated/grpc/health/v1/HealthCheckResponse.ts @@ -0,0 +1,37 @@ +// Original file: proto/health/v1/health.proto + + +// Original file: proto/health/v1/health.proto + +export const _grpc_health_v1_HealthCheckResponse_ServingStatus = { + UNKNOWN: 'UNKNOWN', + SERVING: 'SERVING', + NOT_SERVING: 'NOT_SERVING', + /** + * Used only by the Watch method. + */ + SERVICE_UNKNOWN: 'SERVICE_UNKNOWN', +} as const; + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus = + | 'UNKNOWN' + | 0 + | 'SERVING' + | 1 + | 'NOT_SERVING' + | 2 + /** + * Used only by the Watch method. + */ + | 'SERVICE_UNKNOWN' + | 3 + +export type _grpc_health_v1_HealthCheckResponse_ServingStatus__Output = typeof _grpc_health_v1_HealthCheckResponse_ServingStatus[keyof typeof _grpc_health_v1_HealthCheckResponse_ServingStatus] + +export interface HealthCheckResponse { + 'status'?: (_grpc_health_v1_HealthCheckResponse_ServingStatus); +} + +export interface HealthCheckResponse__Output { + 'status': (_grpc_health_v1_HealthCheckResponse_ServingStatus__Output); +} diff --git a/packages/grpc-health-check/test/generated/health.ts b/packages/grpc-health-check/test/generated/health.ts new file mode 100644 index 000000000..afb2ced5f --- /dev/null +++ b/packages/grpc-health-check/test/generated/health.ts @@ -0,0 +1,26 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { MessageTypeDefinition } from '@grpc/proto-loader'; + +import type { HealthClient as _grpc_health_v1_HealthClient, HealthDefinition as _grpc_health_v1_HealthDefinition } from './grpc/health/v1/Health'; + +type SubtypeConstructor any, Subtype> = { + new(...args: ConstructorParameters): Subtype; +}; + +export interface ProtoGrpcType { + grpc: { + health: { + v1: { + /** + * 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. + */ + Health: SubtypeConstructor & { service: _grpc_health_v1_HealthDefinition } + HealthCheckRequest: MessageTypeDefinition + HealthCheckResponse: MessageTypeDefinition + } + } + } +} + From 524bb7d34142d8e8bfeb120b40e9a06ae6c9ad35 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 18 Sep 2023 14:55:16 -0700 Subject: [PATCH 2/2] grpc-health-check: Implement version 2.0 update --- packages/grpc-health-check/README.md | 34 ++-- packages/grpc-health-check/gulpfile.ts | 28 ++-- packages/grpc-health-check/health.js | 55 ------- packages/grpc-health-check/package.json | 27 ++-- .../proto/health/v1/health.proto | 73 +++++++++ packages/grpc-health-check/src/health.ts | 112 +++++++++++++ .../grpc-health-check/src/object-stream.ts | 75 +++++++++ packages/grpc-health-check/src/server-type.ts | 103 ++++++++++++ .../grpc-health-check/test/health_test.js | 103 ------------ .../grpc-health-check/test/test-health.ts | 152 ++++++++++++++++++ packages/grpc-health-check/tsconfig.json | 29 ++++ 11 files changed, 599 insertions(+), 192 deletions(-) delete mode 100644 packages/grpc-health-check/health.js create mode 100644 packages/grpc-health-check/proto/health/v1/health.proto create mode 100644 packages/grpc-health-check/src/health.ts create mode 100644 packages/grpc-health-check/src/object-stream.ts create mode 100644 packages/grpc-health-check/src/server-type.ts delete mode 100644 packages/grpc-health-check/test/health_test.js create mode 100644 packages/grpc-health-check/test/test-health.ts create mode 100644 packages/grpc-health-check/tsconfig.json diff --git a/packages/grpc-health-check/README.md b/packages/grpc-health-check/README.md index 62a88347f..659dab140 100644 --- a/packages/grpc-health-check/README.md +++ b/packages/grpc-health-check/README.md @@ -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); + +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 diff --git a/packages/grpc-health-check/gulpfile.ts b/packages/grpc-health-check/gulpfile.ts index 0ddaa257e..f47087b14 100644 --- a/packages/grpc-health-check/gulpfile.ts +++ b/packages/grpc-health-check/gulpfile.ts @@ -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 -} \ No newline at end of file +} diff --git a/packages/grpc-health-check/health.js b/packages/grpc-health-check/health.js deleted file mode 100644 index cfa9c8348..000000000 --- a/packages/grpc-health-check/health.js +++ /dev/null @@ -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 -}; diff --git a/packages/grpc-health-check/package.json b/packages/grpc-health-check/package.json index e9b836346..a7fe1c3fd 100644 --- a/packages/grpc-health-check/package.json +++ b/packages/grpc-health-check/package.json @@ -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" + } } diff --git a/packages/grpc-health-check/proto/health/v1/health.proto b/packages/grpc-health-check/proto/health/v1/health.proto new file mode 100644 index 000000000..13b03f567 --- /dev/null +++ b/packages/grpc-health-check/proto/health/v1/health.proto @@ -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); +} diff --git a/packages/grpc-health-check/src/health.ts b/packages/grpc-health-check/src/health.ts new file mode 100644 index 000000000..86ca1af0d --- /dev/null +++ b/packages/grpc-health-check/src/health.ts @@ -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 = new Map(); + private watchers: Map> = 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(); + 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, callback: sendUnaryData) => { + 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) => { + 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'); diff --git a/packages/grpc-health-check/src/object-stream.ts b/packages/grpc-health-check/src/object-stream.ts new file mode 100644 index 000000000..2f70cfa7e --- /dev/null +++ b/packages/grpc-health-check/src/object-stream.ts @@ -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 { + 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 extends Readable { + read(size?: number): any & T; +} + +export type ObjectReadable = { + read(size?: number): T; +} & EmitterAugmentation1<'data', T> & + IntermediateObjectReadable; + +export interface IntermediateObjectWritable 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 extends Writable ? this : void; + end( + chunk: any & T, + cb?: Function + ): ReturnType extends Writable ? this : void; + end( + chunk: any & T, + encoding?: any, + cb?: Function + ): ReturnType extends Writable ? this : void; +} + +export interface ObjectWritable extends IntermediateObjectWritable { + _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 extends Writable ? this : void; + end( + chunk: T, + cb?: Function + ): ReturnType extends Writable ? this : void; + end( + chunk: T, + encoding?: any, + cb?: Function + ): ReturnType extends Writable ? this : void; +} diff --git a/packages/grpc-health-check/src/server-type.ts b/packages/grpc-health-check/src/server-type.ts new file mode 100644 index 000000000..f07704e87 --- /dev/null +++ b/packages/grpc-health-check/src/server-type.ts @@ -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; + +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 = ServerSurfaceCall & { + request: RequestType; +}; +type ServerReadableStream = + ServerSurfaceCall & ObjectReadable; +export type ServerWritableStream = + ServerSurfaceCall & + ObjectWritable & { + request: RequestType; + end: (metadata?: Metadata) => void; + }; +type ServerDuplexStream = ServerSurfaceCall & + ObjectReadable & + ObjectWritable & { end: (metadata?: Metadata) => void }; + +// Unary response callback signature. +export type sendUnaryData = ( + error: ServerErrorResponse | ServerStatusResponse | null, + value?: ResponseType | null, + trailer?: Metadata, + flags?: number +) => void; + +// User provided handler for unary calls. +type handleUnaryCall = ( + call: ServerUnaryCall, + callback: sendUnaryData +) => void; + +// User provided handler for client streaming calls. +type handleClientStreamingCall = ( + call: ServerReadableStream, + callback: sendUnaryData +) => void; + +// User provided handler for server streaming calls. +type handleServerStreamingCall = ( + call: ServerWritableStream +) => void; + +// User provided handler for bidirectional streaming calls. +type handleBidiStreamingCall = ( + call: ServerDuplexStream +) => void; + +export type HandleCall = + | handleUnaryCall + | handleClientStreamingCall + | handleServerStreamingCall + | handleBidiStreamingCall; + +export type UntypedHandleCall = HandleCall; +export interface UntypedServiceImplementation { + [name: string]: UntypedHandleCall; +} + +export interface Server { + addService(service: ServiceDefinition, implementation: UntypedServiceImplementation): void; +} diff --git a/packages/grpc-health-check/test/health_test.js b/packages/grpc-health-check/test/health_test.js deleted file mode 100644 index a31d3b371..000000000 --- a/packages/grpc-health-check/test/health_test.js +++ /dev/null @@ -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(); - }); - }); - }); -}); diff --git a/packages/grpc-health-check/test/test-health.ts b/packages/grpc-health-check/test/test-health.ts new file mode 100644 index 000000000..80d60a234 --- /dev/null +++ b/packages/grpc-health-check/test/test-health.ts @@ -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(); + }); + }) + }); +}); diff --git a/packages/grpc-health-check/tsconfig.json b/packages/grpc-health-check/tsconfig.json new file mode 100644 index 000000000..763ceda98 --- /dev/null +++ b/packages/grpc-health-check/tsconfig.json @@ -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" + ] +}