Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO-NOT-MERGE] spike - gRPC support #571

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/authentication/test/acceptance/basic-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('Basic Authentication', () => {
}
}
// bind user defined sequence
app.sequence(MySequence);
app.httpSequence(MySequence);
}

function givenProviders() {
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@types/js-yaml": "^3.9.1",
"body": "^5.1.0",
"debug": "^2.6.0",
"grpc": "^1.6.0",
"http-errors": "^1.6.1",
"js-yaml": "^3.9.1",
"lodash": "^4.17.4",
Expand Down
157 changes: 150 additions & 7 deletions packages/core/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ import {
RouteEntry,
createEmptyApiSpec,
parseOperationArgs,
UnaryResult,
} from '.';
import {ServerRequest, ServerResponse, createServer} from 'http';
import {Component, mountComponent} from './component';
import {getControllerSpec} from './router/metadata';
import {HttpHandler} from './http-handler';
import {writeResultToResponse} from './writer';
import {DefaultSequence, SequenceHandler, SequenceFunction} from './sequence';
import {
DefaultSequence,
SequenceHandler as HttpSequenceHandler,
SequenceFunction,
} from './sequence';
import {RejectProvider} from './router/providers/reject';
import {
FindRoute,
Expand All @@ -44,6 +49,11 @@ import {InvokeMethodProvider} from './router/providers/invoke-method';
import {FindRouteProvider} from './router/providers/find-route';
import {CoreBindings} from './keys';

import {
SequenceHandler as GrpcSequenceHandler,
DefaultSequence as DefaultGrpcSequence,
} from './grpc/sequence';

const SequenceActions = CoreBindings.SequenceActions;

// NOTE(bajtos) we cannot use `import * as cloneDeep from 'lodash/cloneDeep'
Expand All @@ -54,6 +64,8 @@ const cloneDeep: <T>(value: T) => T = require('lodash/cloneDeep');

const debug = require('debug')('loopback:core:application');

const grpc = require('grpc');

interface OpenApiSpecOptions {
version?: string;
format?: string;
Expand Down Expand Up @@ -100,6 +112,9 @@ export class Application extends Context {
this.bind(CoreBindings.HTTP_PORT).to(
options.http ? options.http.port : 3000,
);
this.bind(CoreBindings.Grpc.PORT).to(
options.grpc ? options.grpc.port : 50051,
);
this.api(createEmptyApiSpec());

if (options.components) {
Expand All @@ -108,7 +123,7 @@ export class Application extends Context {
}
}

this.sequence(options.sequence ? options.sequence : DefaultSequence);
this.httpSequence(options.sequence ? options.sequence : DefaultSequence);

this.handleHttp = (req: ServerRequest, res: ServerResponse) => {
try {
Expand All @@ -129,6 +144,8 @@ export class Application extends Context {
this.bind(SequenceActions.REJECT).toProvider(RejectProvider);
this.bind(CoreBindings.GET_FROM_CONTEXT).toProvider(GetFromContextProvider);
this.bind(CoreBindings.BIND_ELEMENT).toProvider(BindElementProvider);

this.grpcSequence(DefaultGrpcSequence);
}

protected _handleHttpRequest(
Expand Down Expand Up @@ -435,8 +452,12 @@ export class Application extends Context {
*
* @param value The sequence to invoke for each incoming request.
*/
public sequence(value: Constructor<SequenceHandler>) {
this.bind(CoreBindings.SEQUENCE).toClass(value);
public httpSequence(value: Constructor<HttpSequenceHandler>) {
this.bind(CoreBindings.Http.SEQUENCE).toClass(value);
}

public grpcSequence(value: Constructor<GrpcSequenceHandler>) {
this.bind(CoreBindings.Grpc.SEQUENCE).toClass(value);
}

/**
Expand Down Expand Up @@ -475,13 +496,13 @@ export class Application extends Context {
}
}

this.sequence(SequenceFromFunction);
this.httpSequence(SequenceFromFunction);
}

/**
* Start the application (e.g. HTTP/HTTPS servers).
*/
async start(): Promise<void> {
async _startHttp(): Promise<void> {
// Setup the HTTP handler so that we can verify the configuration
// of API spec, controllers and routes at startup time.
this._setupHandlerIfNeeded();
Expand All @@ -502,6 +523,123 @@ export class Application extends Context {
});
}

async start(): Promise<void> {
await Promise.all([
this._startHttp(),
this._startGrpc(),
]);
}

async _startGrpc(): Promise<void> {
debug('Setting up gRPC server');
const server = new grpc.Server();
let hasGrpcServices = false;

for (const b of this.find('controllers.*')) {
const controllerName = b.key.replace(/^controllers\./, '');
const ctor = b.valueConstructor;
if (!ctor) {
throw new Error(
`The controller ${controllerName} was not bound via .toClass()`);
}

// tslint:disable-next-line:no-any
const spec: any = (ctor as {grpcService?: object}).grpcService;
if (!spec) {
debug(
` skipping controller ${controllerName} - no gRPC API was specified`,
);
continue;
}

debug(' adding controller %s with spec %j', controllerName, spec);
for (const key in spec) {
const methodName = key;
const opSpec = spec[key];
// FIXME(bajtos) handle the case when the controller method is using
// opSpec.originalName

if (!ctor.prototype[methodName]) {
debug(
' - skipping method %s - %s.%s not implemented',
opSpec.originalName,
controllerName,
methodName);
continue;
}

debug(
' - added %s as %s.%s',
opSpec.originalName,
controllerName,
methodName,
);

// TODO: support stream method types
server.register(
opSpec.path,
createUnaryHandlerFor(ctor, controllerName, methodName, this),
opSpec.responseSerialize,
opSpec.requestDeserialize,
'unary',
);
hasGrpcServices = true;
}
}

function createUnaryHandlerFor(
// tslint:disable-next-line:no-any
controllerCtor: Constructor<any>,
controllerName: string,
methodName: string,
rootContext: Context,
) {
// tslint:disable-next-line:no-any
return function(request: any, callback: any) {
debug('gRPC invoke %s.%s(%j)', controllerName, methodName, request);
handleUnary().then(
result => callback(null, result.value, result.trailer, result.flags),
error => {
debugger;
callback(error);
},
);

async function handleUnary(): Promise<UnaryResult> {
const context = new Context(rootContext);
context.bind(CoreBindings.Grpc.CONTEXT).to(context);
context.bind(CoreBindings.CONTROLLER_NAME).to(controllerName);
context.bind(CoreBindings.CONTROLLER_CLASS).to(controllerCtor);
context.bind(CoreBindings.CONTROLLER_METHOD_NAME).to(methodName);
const sequence: GrpcSequenceHandler = await context.get(
CoreBindings.Grpc.SEQUENCE,
);
return sequence.handleUnaryCall(request);
}
};
}

if (!hasGrpcServices) {
debug('No gRPC services are configured - server not started.');
return;
}

// TODO(bajtos) Make the hostname configurable
const bindPort = await this.getSync(CoreBindings.Grpc.PORT);
const port = server.bind(
`0.0.0.0:${bindPort}`,
grpc.ServerCredentials.createInsecure(),
);
if (!port) {
throw new Error(`Cannot start gRPC server on port ${bindPort}`);
}
debug(`GRPC server listening at port ${port}`);
this.bind('grpc.port').to(port);

// NOTE(bajtos) Looks like gRPC server starts synchronously
server.start();
}

protected _onUnhandledError(
req: ServerRequest,
res: ServerResponse,
Expand All @@ -523,10 +661,15 @@ export class Application extends Context {

export interface ApplicationOptions {
http?: HttpConfig;
grpc?: GrpcConfig;
components?: Array<Constructor<Component>>;
sequence?: Constructor<SequenceHandler>;
sequence?: Constructor<HttpSequenceHandler>;
}

export interface HttpConfig {
port: number;
}

export interface GrpcConfig {
port: number;
}
3 changes: 3 additions & 0 deletions packages/core/src/grpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# gRPC transport for Controllers (a spike)

This directory contains a very basic implementation of gRPC transport. The real implementation would live in its own packages directory, e.g. `packages/grpc`.
13 changes: 13 additions & 0 deletions packages/core/src/grpc/decorators/service.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright IBM Corp. 2017. All Rights Reserved.
// Node module: @loopback/core
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

// tslint:disable:no-any

// TODO(bajtos) serviceSpec needs to be typed
export function service(serviceSpec: any) {
return function(controllerCtor: Function) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it important that we append the property to the target as-is, or can we extend it with the decorator?

export function service(serviceSpec: any) {
  return function newClass<T extends {new(...args: any[]):{}}>(target: T) {
      return class extends target {
        grpcService: any = serviceSpec;
      }   
  }
};

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I think it should be ok to return a new class in the decorator. But then what are the downsides of appending the static property the existing controller class? Is the extra complexity worth it?

One of my concerns is that controllers decorated with @services will have anonymous class name or the same class name newClass for all controllers. This will make (error) stack traces rather unhelpful, as we are already experiencing with juggler models that all have the sam class name ModelConstructor (see lib/model-builder.js).

(controllerCtor as any).grpcService = serviceSpec;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it a good practice to mutate the class directly instead of Reflector?

Copy link
Member Author

Choose a reason for hiding this comment

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

I see two issues with Reflector:

  • It relies on having a single reflect-metadata package in node_modules tree, which cannot be reliably achieved.
  • It's difficult to use from JavaScript codebase.

Creating a static property on the constructor class solves both issues and it's the same approach we are already using for Model.definition in our repository package. I think we should eventually use this approach for storing OpenAPI spec too.

Thoughts?

cc @kjdelisle @virkt25

Copy link
Contributor

@kjdelisle kjdelisle Sep 14, 2017

Choose a reason for hiding this comment

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

+1, for the first reason alone; if others are making use of reflect-metadata, which is entirely likely, it could impact us.

};
}
2 changes: 1 addition & 1 deletion packages/core/src/http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class HttpHandler {
const requestContext = this._createRequestContext(request, response);

const sequence: SequenceHandler = await requestContext.get(
CoreBindings.SEQUENCE,
CoreBindings.Http.SEQUENCE,
);
await sequence.handle(parsedRequest, response);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ import * as HttpErrors from 'http-errors';
// http errors
export {HttpErrors};

export * from './grpc/decorators/service.decorator';
export * from './grpc/grpc-types';
import {
SequenceHandler as GrpcSequenceHandler,
DefaultSequence as GrpcDefaultSequence,
} from './grpc/sequence';
export {GrpcSequenceHandler, GrpcDefaultSequence};
// NOTE(bajtos) ^^ that's a hack for this spike so that I don't have to start
// a new grpc package yet. This will not make it into `master` branch.

// internals used by unit-tests
export {
ParsedRequest,
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export namespace CoreBindings {
// application-wide bindings

export const HTTP_PORT = 'http.port';
export const GRPC_PORT = 'grpc.port';
export const HTTP_HANDLER = 'http.handler';

export const API_SPEC = 'application.apiSpec';
export const SEQUENCE = 'sequence';

export namespace SequenceActions {
export const FIND_ROUTE = 'sequence.actions.findRoute';
Expand All @@ -26,13 +26,21 @@ export namespace CoreBindings {

// request-specific bindings

export const CONTROLLER_NAME = 'controller.current.name';
export const CONTROLLER_CLASS = 'controller.current.ctor';
export const CONTROLLER_METHOD_NAME = 'controller.current.operation';
export const CONTROLLER_METHOD_META = 'controller.method.meta';

export namespace Http {
export const SEQUENCE = 'http.sequence';
export const REQUEST = 'http.request';
export const RESPONSE = 'http.response';
export const CONTEXT = 'http.request.context';
}

export namespace Grpc {
export const PORT = 'grpc.port';
export const CONTEXT = 'grpc.request.context';
export const SEQUENCE = 'grpc.sequence';
}
}
2 changes: 1 addition & 1 deletion packages/core/src/sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface SequenceHandler {
*
* User can bind their own Sequence to app as shown below
* ```ts
* app.bind(CoreBindings.SEQUENCE).toClass(MySequence);
* app.bind(CoreBindings.Http.SEQUENCE).toClass(MySequence);
* ```
*/
export class DefaultSequence implements SequenceHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Bootstrapping the application', () => {
before(givenAppWithUserDefinedSequence);

it('binds the `sequence` key to the user-defined sequence', async () => {
const binding = await app.get(CoreBindings.SEQUENCE);
const binding = await app.get(CoreBindings.Http.SEQUENCE);
expect(binding.constructor.name).to.equal('UserDefinedSequence');
});

Expand Down
Loading