Skip to content

Commit

Permalink
Make service registration extensible.
Browse files Browse the repository at this point in the history
This introduces the 'ServiceBundle' interface to pass a set of services to the
server/endpoint.

To make this more clean, this also introduces the 'ServiceEndpoint' interface shared
by the HTTP/2 server and LambdaEventHandler to extract the common registration parts.
  • Loading branch information
StephanEwen committed Feb 7, 2024
1 parent c48f9bf commit 6cdb3fd
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 100 deletions.
6 changes: 5 additions & 1 deletion src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export {
SendClient,
} from "./types/router";
export { RestateServer, createServer } from "./server/restate_server";
export { ServiceOpts } from "./server/base_restate_server";
export {
ServiceOpts,
ServiceBundle,
ServiceEndpoint,
} from "./server/base_restate_server";
export {
LambdaRestateServer,
createLambdaApiGatewayHandler,
Expand Down
89 changes: 88 additions & 1 deletion src/server/base_restate_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,103 @@ import {
import { RestateContext, RpcContext, useContext } from "../restate_context";
import { verifyAssumptions } from "../utils/assumptions";
import { TerminalError } from "../public_api";
import { isEventHandler } from "../types/router";
import { KeyedRouter, UnKeyedRouter, isEventHandler } from "../types/router";
import { jsonSafeAny } from "../utils/utils";
import { rlog } from "../logger";

/**
* The properties describing a gRPC service. Consisting of the name, the object holding the
* implementation, and the descriptor (metadata) describing the service and types.
*/
export interface ServiceOpts {
descriptor: ProtoMetadata;
service: string;
instance: unknown;
}

/**
* The ServiceEndpoint the where Restate services are registered.
* They will be invoked depending on the concrete server implementation, e.g.,
* by HTTP/2 calls, AWS Lambda events, etc.
*/
export interface ServiceEndpoint {
/**
* Adds a gRPC service to be served from this endpoint.
*
* The {@link ServiceOpts} passed here need to describe the following properties:
*
* - The 'service' name: the name of the gRPC service (as in the service definition proto file).
* - The service 'instance': the implementation of the service logic (must implement the generated
* gRPC service interface).
* - The gRPC/protobuf 'descriptor': The protoMetadata descriptor that describes the service, methods,
* and parameter types. It is usually found as the value 'protoMetadata' in the generated
* file '(service-name).ts'
*
* The descriptor is generated by the protobuf compiler and needed by Restate to reflectively discover
* the service details, understand payload serialization, perform HTTP/JSON-to-gRPC transcoding, or
* to proxy the service.
*
* If you define multiple services in the same '.proto' file, you may have only one descriptor that
* describes all services together. You can pass the same descriptor to multiple calls of '.bindService()'.
*
* If you don't find the gRPC/protobuf descriptor, make your you generated the gRPC/ProtoBuf code with
* the option to generate the descriptor. For example, using the 'ts-proto' plugin, make sure you pass
* the 'outputSchema=true' option. If you are using Restate's project templates, this should all be
* pre-configured for you.
*
* @example
* ```
* endpoint.bindService({
* service: "MyService",
* instance: new myService.MyServiceImpl(),
* descriptor: myService.protoMetadata
* })
* ```
*
* @param serviceOpts The options describing the service to be bound. See above for a detailed description.
* @returns An instance of this LambdaRestateServer
*/
bindService(serviceOpts: ServiceOpts): ServiceEndpoint;

/**
* Binds a new durable RPC service to the given path. This method is for regular (stateless)
* durably executed RPC services.
*
* The service will expose all properties of the router that are functions as follows:
* If the path is 'acme.myservice' and the router has '{ foo, bar }' as properties, the
* Restate will expose the RPC paths '/acme.myservice/foo' and '/acme.myservice/bar'.
*/
bindRouter<M>(path: string, router: UnKeyedRouter<M>): ServiceEndpoint;

/**
* Binds a new stateful keyed durable RPC service to the given path.
* This method is services where each invocation is bound to a key and that may maintain
* state per key.
*
* The service will expose all properties of the router that are functions as follows:
* If the path is 'acme.myservice' and the router has '{ foo, bar }' as properties, the
* Restate will expose the RPC paths '/acme.myservice/foo' and '/acme.myservice/bar'.
*/
bindKeyedRouter<M>(path: string, router: KeyedRouter<M>): ServiceEndpoint;

/**
* Adds one or more services to this endpoint. This will call the
* {@link ServiceBundle.registerServices} function to register all services at this endpoint.
*/
bind(services: ServiceBundle): ServiceEndpoint;
}

/**
* Utility interface for a bundle of one or more services belonging together
* and being registered together.
*/
export interface ServiceBundle {
/**
* Called to register the services at the endpoint.
*/
registerServices(endpoint: ServiceEndpoint): void;
}

export abstract class BaseRestateServer {
protected readonly methods: Record<
string,
Expand Down
116 changes: 56 additions & 60 deletions src/server/restate_lambda_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import {
ProtocolMode,
ServiceDiscoveryResponse,
} from "../generated/proto/discovery";
import { BaseRestateServer, ServiceOpts } from "./base_restate_server";
import {
BaseRestateServer,
ServiceBundle,
ServiceEndpoint,
ServiceOpts,
} from "./base_restate_server";
import { LambdaConnection } from "../connection/lambda_connection";
import { InvocationBuilder } from "../invocation";
import { decodeLambdaBody } from "../io/decoder";
Expand Down Expand Up @@ -53,58 +58,67 @@ import { OUTPUT_STREAM_ENTRY_MESSAGE_TYPE } from "../types/protocol";
* ```
*/
export function createLambdaApiGatewayHandler(): LambdaRestateServer {
return new LambdaRestateServer();
return new LambdaRestateServerImpl();
}

/**
* Restate entrypoint implementation for services deployed on AWS Lambda.
* This one decodes the requests, create the log event sequence that
* drives the durable execution of the service invocations.
*/
export class LambdaRestateServer extends BaseRestateServer {
constructor() {
super(ProtocolMode.REQUEST_RESPONSE);
}

export interface LambdaRestateServer extends ServiceEndpoint {
/**
* Adds a gRPC service to be served from this endpoint.
*
* The {@link ServiceOpts} passed here need to describe the following properties:
*
* - The 'service' name: the name of the gRPC service (as in the service definition proto file).
* - The service 'instance': the implementation of the service logic (must implement the generated
* gRPC service interface).
* - The gRPC/protobuf 'descriptor': The protoMetadata descriptor that describes the service, methods,
* and parameter types. It is usually found as the value 'protoMetadata' in the generated
* file '(service-name).ts'
*
* The descriptor is generated by the protobuf compiler and needed by Restate to reflectively discover
* the service details, understand payload serialization, perform HTTP/JSON-to-gRPC transcoding, or
* to proxy the service.
*
* If you define multiple services in the same '.proto' file, you may have only one descriptor that
* describes all services together. You can pass the same descriptor to multiple calls of '.bindService()'.
* Creates the invocation handler function to be called by AWS Lambda.
*
* If you don't find the gRPC/protobuf descriptor, make your you generated the gRPC/ProtoBuf code with
* the option to generate the descriptor. For example, using the 'ts-proto' plugin, make sure you pass
* the 'outputSchema=true' option. If you are using Restate's project templates, this should all be
* pre-configured for you.
* The returned type of this function is `(event: APIGatewayProxyEvent | APIGatewayProxyEventV2) => Promise<APIGatewayProxyResult | APIGatewayProxyResultV2>`.
* We use `any` types here to avoid a dependency on the `@types/aws-lambda` dependency for consumers of this API.
*
* @example
* A typical AWS Lambda entry point would use this method the follwing way:
* ```
* endpoint.bindService({
* service: "MyService",
* instance: new myService.MyServiceImpl(),
* descriptor: myService.protoMetadata
* })
* import * as restate from "@restatedev/restate-sdk";
*
* export const handler = restate
* .createLambdaApiGatewayHandler()
* .bindService({
* service: "MyService",
* instance: new myService.MyServiceImpl(),
* descriptor: myService.protoMetadata,
* })
* .handle();
* ```
*
* @param serviceOpts The options describing the service to be bound. See above for a detailed description.
* @returns An instance of this LambdaRestateServer
* @returns The invocation handler function for to be called by AWS Lambda.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handle(): (event: any) => Promise<any>;

// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bindService(serviceOpts: ServiceOpts): LambdaRestateServer;

// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bindRouter<M>(path: string, router: UnKeyedRouter<M>): LambdaRestateServer;

// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bindKeyedRouter<M>(path: string, router: KeyedRouter<M>): LambdaRestateServer;

// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bind(services: ServiceBundle): LambdaRestateServer;
}

class LambdaRestateServerImpl
extends BaseRestateServer
implements LambdaRestateServer
{
constructor() {
super(ProtocolMode.REQUEST_RESPONSE);
}

public bindService(serviceOpts: ServiceOpts): LambdaRestateServer {
// Implementation note: This override if here mainly to change the return type to the more
// concrete type LambdaRestateServer (from BaseRestateServer).
super.bindService(serviceOpts);
return this;
}
Expand All @@ -125,29 +139,11 @@ export class LambdaRestateServer extends BaseRestateServer {
return this;
}

/**
* Creates the invocation handler function to be called by AWS Lambda.
*
* The returned type of this function is `(event: APIGatewayProxyEvent | APIGatewayProxyEventV2) => Promise<APIGatewayProxyResult | APIGatewayProxyResultV2>`.
* We use `any` types here to avoid a dependency on the `@types/aws-lambda` dependency for consumers of this API.
*
* @example
* A typical AWS Lambda entry point would use this method the follwing way:
* ```
* import * as restate from "@restatedev/restate-sdk";
*
* export const handler = restate
* .createLambdaApiGatewayHandler()
* .bindService({
* service: "MyService",
* instance: new myService.MyServiceImpl(),
* descriptor: myService.protoMetadata,
* })
* .handle();
* ```
*
* @returns The invocation handler function for to be called by AWS Lambda.
*/
public bind(services: ServiceBundle): LambdaRestateServer {
services.registerServices(this);
return this;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handle(): (event: any) => Promise<any> {
// return the handler and bind the current context to it, so that it can find the other methods in this class.
Expand Down
60 changes: 22 additions & 38 deletions src/server/restate_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import {
ProtocolMode,
ServiceDiscoveryResponse,
} from "../generated/proto/discovery";
import { BaseRestateServer, ServiceOpts } from "./base_restate_server";
import {
BaseRestateServer,
ServiceBundle,
ServiceEndpoint,
ServiceOpts,
} from "./base_restate_server";
import { RestateHttp2Connection } from "../connection/http_connection";
import { HostedGrpcServiceMethod } from "../types/grpc";
import { ensureError } from "../types/errors";
Expand All @@ -26,52 +31,26 @@ import { StateMachine } from "../state_machine";
import { KeyedRouter, UnKeyedRouter } from "../types/router";
import { rlog } from "../logger";

export interface RestateServer {
export interface RestateServer extends ServiceEndpoint {
// RestateServer is a http2 server handler that you can pass to http2.createServer.
(request: Http2ServerRequest, response: Http2ServerResponse): void;

/**
* Adds a gRPC service to be served from this endpoint.
*
* The {@link ServiceOpts} passed here need to describe the following properties:
*
* - The 'service' name: the name of the gRPC service (as in the service definition proto file).
* - The service 'instance': the implementation of the service logic (must implement the generated
* gRPC service interface).
* - The gRPC/protobuf 'descriptor': The protoMetadata descriptor that describes the service, methods,
* and parameter types. It is usually found as the value 'protoMetadata' in the generated
* file '(service-name).ts'
*
* The descriptor is generated by the protobuf compiler and needed by Restate to reflectively discover
* the service details, understand payload serialization, perform HTTP/JSON-to-gRPC transcoding, or
* to proxy the service.
*
* If you define multiple services in the same '.proto' file, you may have only one descriptor that
* describes all services together. You can pass the same descriptor to multiple calls of '.bindService()'.
*
* If you don't find the gRPC/protobuf descriptor, make your you generated the gRPC/ProtoBuf code with
* the option to generate the descriptor. For example, using the 'ts-proto' plugin, make sure you pass
* the 'outputSchema=true' option. If you are using Restate's project templates, this should all be
* pre-configured for you.
*
* @example
* ```
* endpoint.bindService({
* service: "MyService",
* instance: new myService.MyServiceImpl(),
* descriptor: myService.protoMetadata
* })
* ```
*
* @param serviceOpts The options describing the service to be bound. See above for a detailed description.
* @returns An instance of this RestateServer
*/
// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bindService(serviceOpts: ServiceOpts): RestateServer;

// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bindKeyedRouter<M>(path: string, router: KeyedRouter<M>): RestateServer;

// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bindRouter<M>(path: string, router: UnKeyedRouter<M>): RestateServer;

// overridden to make return type more specific
// docs are inherited from ServiceEndpoint
bind(services: ServiceBundle): RestateServer;

/**
* Starts the Restate server and listens at the given port.
*
Expand Down Expand Up @@ -147,6 +126,11 @@ export function createServer(): RestateServer {
restateServerImpl.bindService(serviceOpts);
return instance;
};
instance.bind = (services: ServiceBundle) => {
services.registerServices(instance);
return instance;
};

instance.listen = (port?: number) => {
const actualPort = port ?? parseInt(process.env.PORT ?? "9080");
rlog.info(`Listening on ${actualPort}...`);
Expand Down

0 comments on commit 6cdb3fd

Please sign in to comment.