Skip to content

Commit

Permalink
feat(cmd-api-server): add gRPC plugin auto-registration support
Browse files Browse the repository at this point in the history
1. The API server supports gRPC endpoints, but plugins are not yet able
to register their own gRPC services to be exposed the same way that was
already possible for HTTP endpoints to be registered dynamically. This
was due to an oversight when the original contribution was made by Peter
(who was the person making the oversight - good job Peter)
2. The functionality works largely the same as it does for the HTTP
endpoints but it does so for gRPC services (which is the equivalent of
endpoints in gRPC terminology, so service === endpoint in this context.)
3. There are new methods added to the public API surface of the API server
package which can be used to construct gRPC credential and server objects
using the instance of the library that is used by the API server.
This is necessary because the validation logic built into grpc-js fails
for these mentioned objects if the creds or the server was constructed
with a different instance of the library than the one used by the API
server.
4. Different instance in this context means just that the exact same
version of the library was imported from a different path for example
there could be the node_modules directory of the besu connector and also
the node_modules directory of the API server.
5. Because of the problem outlined above, the only way we can have functioning
test cases is if the API server exposes its own instance of grpc-js.

Signed-off-by: Peter Somogyvari <[email protected]>
  • Loading branch information
petermetz committed Apr 4, 2024
1 parent 7951784 commit 5762dad
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 5 deletions.
54 changes: 49 additions & 5 deletions packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
PluginImport,
Constants,
PluginImportAction,
isIPluginGrpcService,
} from "@hyperledger/cactus-core-api";

import {
Expand Down Expand Up @@ -105,6 +106,7 @@ export class ApiServer {
private readonly enableShutdownHook: boolean;

public prometheusExporter: PrometheusExporter;
public boundGrpcHostPort: string;

public get className(): string {
return ApiServer.CLASS_NAME;
Expand All @@ -118,6 +120,8 @@ export class ApiServer {
throw new Error(`ApiServer#ctor options.config was falsy`);
}

this.boundGrpcHostPort = "127.0.0.1:-1";

this.enableShutdownHook = Bools.isBooleanStrict(
options.config.enableShutdownHook,
)
Expand Down Expand Up @@ -462,20 +466,24 @@ export class ApiServer {
}

if (this.grpcServer) {
this.log.info(`Closing Cacti gRPC server ...`);
await new Promise<void>((resolve, reject) => {
this.log.info(`Draining Cacti gRPC server ...`);
this.grpcServer.drain(this.boundGrpcHostPort, 5000);
this.log.info(`Drained Cacti gRPC server OK`);

this.log.info(`Trying to shut down Cacti gRPC server ...`);
this.grpcServer.tryShutdown((ex?: Error) => {
if (ex) {
const eMsg =
"Failed to shut down gRPC server of the Cacti API server.";
this.log.debug(eMsg, ex);
reject(newRex(eMsg, ex));
} else {
this.log.info(`Shut down Cacti gRPC server OK`);
resolve();
}
});
});
this.log.info(`Close gRPC server OK`);
}
}

Expand Down Expand Up @@ -648,6 +656,11 @@ export class ApiServer {
}

async startGrpcServer(): Promise<AddressInfo> {
const fnTag = `${this.className}#startGrpcServer()`;
const { log } = this;
const { logLevel } = this.options.config;
const pluginRegistry = await this.getOrInitPluginRegistry();

return new Promise((resolve, reject) => {
// const grpcHost = "0.0.0.0"; // FIXME - make this configurable (config-service.ts)
const grpcHost = "127.0.0.1"; // FIXME - make this configurable (config-service.ts)
Expand All @@ -672,15 +685,46 @@ export class ApiServer {
new GrpcServerApiServer(),
);

log.debug("Installing gRPC services of IPluginGrpcService instances...");
pluginRegistry.getPlugins().forEach(async (x: ICactusPlugin) => {
if (!isIPluginGrpcService(x)) {
this.log.debug("%s skipping %s instance", fnTag, x.getPackageName());
return;
}
const opts = { logLevel };
log.info("%s Creating gRPC service of: %s", fnTag, x.getPackageName());

const svcPairs = await x.createGrpcSvcDefAndImplPairs(opts);
log.debug("%s Obtained %o gRPC svc pairs OK", fnTag, svcPairs.length);

svcPairs.forEach(({ definition, implementation }) => {
const svcNames = Object.values(definition).map((x) => x.originalName);
const svcPaths = Object.values(definition).map((x) => x.path);
log.debug("%s Adding gRPC svc names %o ...", fnTag, svcNames);
log.debug("%s Adding gRPC svc paths %o ...", fnTag, svcPaths);
this.grpcServer.addService(definition, implementation);
log.debug("%s Added gRPC svc OK ...", fnTag);
});

log.info("%s Added gRPC service of: %s OK", fnTag, x.getPackageName());
});
log.debug("%s Installed all IPluginGrpcService instances OK", fnTag);

this.grpcServer.bindAsync(
grpcHostAndPort,
grpcTlsCredentials,
(error: Error | null, port: number) => {
if (error) {
this.log.error("Binding gRPC failed: ", error);
return reject(new RuntimeError("Binding gRPC failed: ", error));
this.log.error("%s Binding gRPC failed: ", fnTag, error);
return reject(new RuntimeError(fnTag + " gRPC bindAsync:", error));
} else {
this.log.info("%s gRPC server bound to port %o OK", fnTag, port);
}
this.grpcServer.start();

const portStr = port.toString(10);
this.boundGrpcHostPort = grpcHost.concat(":").concat(portStr);
log.info("%s boundGrpcHostPort=%s", fnTag, this.boundGrpcHostPort);

const family = determineAddressFamily(grpcHost);
resolve({ address: grpcHost, port, family });
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as grpc from "@grpc/grpc-js";

/**
* Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call
* verbatim.
* Why though? This is necessary because the {grpc.Server} object does an `instanceof`
* validation on credential objects that are passed to it and this check comes back
* negative if you've constructed the credentials object with a different instance
* of the library, **even** if the versions of the library instances are the **same**.
*
* Therefore this is a workaround that allows callers to construct credentials
* objects with the same import of the `@grpc/grpc-js` library that the {ApiServer}
* of this package is using.
*
* @returns {grpc.ServerCredentials}
*/
export function createGrpcInsecureServerCredentials(): grpc.ServerCredentials {
return grpc.ServerCredentials.createInsecure();
}

/**
* Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call
* verbatim.
* Why though? This is necessary because the {grpc.Server} object does an `instanceof`
* validation on credential objects that are passed to it and this check comes back
* negative if you've constructed the credentials object with a different instance
* of the library, **even** if the versions of the library instances are the **same**.
*
* Therefore this is a workaround that allows callers to construct credentials
* objects with the same import of the `@grpc/grpc-js` library that the {ApiServer}
* of this package is using.
*
* @returns {grpc.ServerCredentials}
*/
export function createGrpcSslServerCredentials(
rootCerts: Buffer | null,
keyCertPairs: grpc.KeyCertPair[],
checkClientCertificate?: boolean,
): grpc.ServerCredentials {
return grpc.ServerCredentials.createSsl(
rootCerts,
keyCertPairs,
checkClientCertificate,
);
}

/**
* Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call
* verbatim.
* Why though? This is necessary because the {grpc.Server} object does an `instanceof`
* validation on credential objects that are passed to it and this check comes back
* negative if you've constructed the credentials object with a different instance
* of the library, **even** if the versions of the library instances are the **same**.
*
* Therefore this is a workaround that allows callers to construct credentials
* objects with the same import of the `@grpc/grpc-js` library that the {ApiServer}
* of this package is using.
*
* @returns {grpc.ChannelCredentials}
*/
export function createGrpcInsecureChannelCredentials(): grpc.ChannelCredentials {
return grpc.ChannelCredentials.createInsecure();
}

/**
* Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call
* verbatim.
* Why though? This is necessary because the {grpc.Server} object does an `instanceof`
* validation on credential objects that are passed to it and this check comes back
* negative if you've constructed the credentials object with a different instance
* of the library, **even** if the versions of the library instances are the **same**.
*
* Therefore this is a workaround that allows callers to construct credentials
* objects with the same import of the `@grpc/grpc-js` library that the {ApiServer}
* of this package is using.
*
* @returns {grpc.ChannelCredentials}
*/
export function createGrpcSslChannelCredentials(
rootCerts?: Buffer | null,
privateKey?: Buffer | null,
certChain?: Buffer | null,
verifyOptions?: grpc.VerifyOptions,
): grpc.ChannelCredentials {
return grpc.ChannelCredentials.createSsl(
rootCerts,
privateKey,
certChain,
verifyOptions,
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as grpc from "@grpc/grpc-js";

/**
* Re-exports the underlying `new grpc.Server()` call verbatim.
*
* Why though? This is necessary because the {grpc.Server} object does an `instanceof`
* validation on credential objects that are passed to it and this check comes back
* negative if you've constructed the credentials object with a different instance
* of the library, **even** if the versions of the library instances are the **same**.
*
* Therefore this is a workaround that allows callers to construct credentials
* objects/servers with the same import of the `@grpc/grpc-js` library that the
* {ApiServer} of this package is using internally.
*
* @returns {grpc.Server}
*/
export function createGrpcServer(
options?: grpc.ServerOptions | undefined,
): grpc.Server {
return new grpc.Server(options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ export {
} from "./authzn/authorizer-factory";
export { IAuthorizationConfig } from "./authzn/i-authorization-config";
export { AuthorizationProtocol } from "./config/authorization-protocol";

export {
createGrpcInsecureChannelCredentials,
createGrpcInsecureServerCredentials,
createGrpcSslChannelCredentials,
createGrpcSslServerCredentials,
} from "./grpc/grpc-credentials-factory";

export { createGrpcServer } from "./grpc/grpc-server-factory";

1 comment on commit 5762dad

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 0.05.

Benchmark suite Current: 5762dad Previous: fa27fde Ratio
cmd-api-server_HTTP_GET_getOpenApiSpecV1 550 ops/sec (±1.49%) 573 ops/sec (±1.90%) 1.04
cmd-api-server_gRPC_GetOpenApiSpecV1 355 ops/sec (±1.51%) 349 ops/sec (±1.97%) 0.98

This comment was automatically generated by workflow using github-action-benchmark.

CC: @petermetz

Please sign in to comment.