Skip to content

Commit 31e2c9e

Browse files
Server API ergonomics (#221)
1 parent 799761a commit 31e2c9e

File tree

3 files changed

+129
-99
lines changed

3 files changed

+129
-99
lines changed

src/server/base_restate_server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export abstract class BaseRestateServer {
132132
});
133133
}
134134

135-
protected bindService({ descriptor, service, instance }: ServiceOpts) {
135+
bindService({ descriptor, service, instance }: ServiceOpts) {
136136
const spec = parseService(descriptor, service, instance);
137137
this.addDescriptor(descriptor);
138138

src/server/restate_server.ts

Lines changed: 122 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
* https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
1010
*/
1111

12-
import { on } from "events";
1312
import stream from "stream";
1413
import { pipeline, finished } from "stream/promises";
15-
import http2 from "http2";
14+
import http2, { Http2ServerRequest, Http2ServerResponse } from "http2";
1615
import { parse as urlparse, Url } from "url";
1716
import {
1817
ProtocolMode,
@@ -27,63 +26,9 @@ import { InvocationBuilder } from "../invocation";
2726
import { StateMachine } from "../state_machine";
2827
import { KeyedRouter, UnKeyedRouter } from "../types/router";
2928

30-
/**
31-
* Creates a Restate entrypoint based on a HTTP2 server. The entrypoint will listen
32-
* for requests to the services at a specified port.
33-
*
34-
* This is the entrypoint to be used in most scenarios (standalone, Docker, Kubernetes, ...);
35-
* any deployments that forwards requests to a network endpoint. The prominent exception is
36-
* AWS Lambda, which uses the {@link restate_lambda_handler#lambdaApiGatewayHandler}
37-
* function to create an entry point.
38-
*
39-
* After creating this endpoint, register services on this entrypoint via {@link RestateServer.bindService }
40-
* and start it via {@link RestateServer.listen }.
41-
*
42-
* @example
43-
* A typical entry point would look like this:
44-
* ```
45-
* import * as restate from "@restatedev/restate-sdk";
46-
*
47-
* export const handler = restate
48-
* .createServer()
49-
* .bindService({
50-
* service: "MyService",
51-
* instance: new myService.MyServiceImpl(),
52-
* descriptor: myService.protoMetadata,
53-
* })
54-
* .listen(8000);
55-
* ```
56-
*/
57-
export function createServer(): RestateServer {
58-
return new RestateServer();
59-
}
60-
61-
/**
62-
* Restate entrypoint implementation for services. This server receives and
63-
* decodes the requests, streams events between the service and the Restate runtime,
64-
* and drives the durable execution of the service invocations.
65-
*/
66-
export class RestateServer extends BaseRestateServer {
67-
constructor() {
68-
super(ProtocolMode.BIDI_STREAM);
69-
}
70-
71-
public bindKeyedRouter<M>(
72-
path: string,
73-
router: KeyedRouter<M>
74-
): RestateServer {
75-
// Implementation note: This override if here mainly to change the return type to the more
76-
// concrete type RestateServer (from BaseRestateServer).
77-
super.bindRpcService(path, router, true);
78-
return this;
79-
}
80-
81-
public bindRouter<M>(path: string, router: UnKeyedRouter<M>): RestateServer {
82-
// Implementation note: This override if here mainly to change the return type to the more
83-
// concrete type RestateServer (from BaseRestateServer).
84-
super.bindRpcService(path, router, false);
85-
return this;
86-
}
29+
export interface RestateServer {
30+
// RestateServer is a http2 server handler that you can pass to http2.createServer.
31+
(request: Http2ServerRequest, response: Http2ServerResponse): void;
8732

8833
/**
8934
* Adds a gRPC service to be served from this endpoint.
@@ -121,12 +66,11 @@ export class RestateServer extends BaseRestateServer {
12166
* @param serviceOpts The options describing the service to be bound. See above for a detailed description.
12267
* @returns An instance of this RestateServer
12368
*/
124-
public bindService(serviceOpts: ServiceOpts): RestateServer {
125-
// Implementation note: This override if here mainly to change the return type to the more
126-
// concrete type RestateServer (from BaseRestateServer).
127-
super.bindService(serviceOpts);
128-
return this;
129-
}
69+
bindService(serviceOpts: ServiceOpts): RestateServer;
70+
71+
bindKeyedRouter<M>(path: string, router: KeyedRouter<M>): RestateServer;
72+
73+
bindRouter<M>(path: string, router: UnKeyedRouter<M>): RestateServer;
13074

13175
/**
13276
* Starts the Restate server and listens at the given port.
@@ -137,23 +81,124 @@ export class RestateServer extends BaseRestateServer {
13781
*
13882
* This method's result promise never completes.
13983
*
84+
* This method is a shorthand for:
85+
*
86+
* @example
87+
* ```
88+
* const httpServer = http2.createServer(restateServer);
89+
* httpServer.listen(port);
90+
* ```
91+
*
92+
* If you need to manually control the server lifecycle, we suggest to manually instantiate the http2 server and use this object as request handler.
93+
*
14094
* @param port The port to listen at. May be undefined (see above).
14195
*/
142-
public async listen(port?: number) {
143-
// Infer the port if not specified, or default it
96+
listen(port?: number): Promise<void>;
97+
}
98+
99+
/**
100+
* Creates a Restate entrypoint based on a HTTP2 server. The entrypoint will listen
101+
* for requests to the services at a specified port.
102+
*
103+
* This is the entrypoint to be used in most scenarios (standalone, Docker, Kubernetes, ...);
104+
* any deployments that forwards requests to a network endpoint. The prominent exception is
105+
* AWS Lambda, which uses the {@link restate_lambda_handler#lambdaApiGatewayHandler}
106+
* function to create an entry point.
107+
*
108+
* After creating this endpoint, register services on this entrypoint via {@link RestateServer.bindService }
109+
* and start it via {@link RestateServer.listen }.
110+
*
111+
* @example
112+
* A typical entry point would look like this:
113+
* ```
114+
* import * as restate from "@restatedev/restate-sdk";
115+
*
116+
* export const handler = restate
117+
* .createServer()
118+
* .bindService({
119+
* service: "MyService",
120+
* instance: new myService.MyServiceImpl(),
121+
* descriptor: myService.protoMetadata,
122+
* })
123+
* .listen(8000);
124+
* ```
125+
*/
126+
export function createServer(): RestateServer {
127+
// See https://stackoverflow.com/questions/16508435/implementing-typescript-interface-with-bare-function-signature-plus-other-fields/16508581#16508581
128+
// for more details on how we implement the RestateServer interface.
129+
130+
const restateServerImpl = new RestateServerImpl();
131+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
132+
const instance: any = (
133+
request: Http2ServerRequest,
134+
response: Http2ServerResponse
135+
) => {
136+
restateServerImpl.acceptConnection(request, response);
137+
};
138+
instance.bindKeyedRouter = <M>(path: string, router: UnKeyedRouter<M>) => {
139+
restateServerImpl.bindKeyedRouter(path, router);
140+
return instance;
141+
};
142+
instance.bindRouter = <M>(path: string, router: UnKeyedRouter<M>) => {
143+
restateServerImpl.bindRouter(path, router);
144+
return instance;
145+
};
146+
instance.bindService = (serviceOpts: ServiceOpts) => {
147+
restateServerImpl.bindService(serviceOpts);
148+
return instance;
149+
};
150+
instance.listen = (port?: number) => {
144151
const actualPort = port ?? parseInt(process.env.PORT ?? "9080");
145152
rlog.info(`Listening on ${actualPort}...`);
146153

147-
for await (const connection of incomingConnectionAtPort(actualPort)) {
148-
this.handleConnection(connection.url, connection.stream).catch((e) => {
149-
const error = ensureError(e);
150-
rlog.error(
151-
"Error while handling connection: " + (error.stack ?? error.message)
152-
);
153-
connection.stream.end();
154-
connection.stream.destroy();
155-
});
156-
}
154+
const server = http2.createServer(instance);
155+
server.listen(actualPort);
156+
// eslint-disable-next-line @typescript-eslint/no-empty-function
157+
return new Promise(() => {});
158+
};
159+
160+
return <RestateServer>instance;
161+
}
162+
163+
class RestateServerImpl extends BaseRestateServer {
164+
constructor() {
165+
super(ProtocolMode.BIDI_STREAM);
166+
}
167+
168+
bindKeyedRouter<M>(path: string, router: KeyedRouter<M>) {
169+
// Implementation note: This override if here mainly to change the return type to the more
170+
// concrete type RestateServer (from BaseRestateServer).
171+
super.bindRpcService(path, router, true);
172+
}
173+
174+
bindRouter<M>(path: string, router: UnKeyedRouter<M>) {
175+
// Implementation note: This override if here mainly to change the return type to the more
176+
// concrete type RestateServer (from BaseRestateServer).
177+
super.bindRpcService(path, router, false);
178+
}
179+
180+
bindService(serviceOpts: ServiceOpts) {
181+
// Implementation note: This override if here mainly to change the return type to the more
182+
// concrete type RestateServer (from BaseRestateServer).
183+
super.bindService(serviceOpts);
184+
}
185+
186+
acceptConnection(
187+
request: Http2ServerRequest,
188+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
189+
_response: Http2ServerResponse
190+
) {
191+
const stream = request.stream;
192+
const url: Url = urlparse(request.url ?? "/");
193+
194+
this.handleConnection(url, stream).catch((e) => {
195+
const error = ensureError(e);
196+
rlog.error(
197+
"Error while handling connection: " + (error.stack ?? error.message)
198+
);
199+
stream.end();
200+
stream.destroy();
201+
});
157202
}
158203

159204
private async handleConnection(
@@ -190,25 +235,6 @@ export class RestateServer extends BaseRestateServer {
190235
}
191236
}
192237

193-
async function* incomingConnectionAtPort(port: number) {
194-
const server = http2.createServer();
195-
196-
server.on("error", (err) =>
197-
rlog.error("Error in Restate service endpoint http2 server: " + err)
198-
);
199-
server.listen(port);
200-
201-
let connectionId = 1n;
202-
203-
for await (const [s, h] of on(server, "stream")) {
204-
const stream = s as http2.ServerHttp2Stream;
205-
const headers = h as http2.IncomingHttpHeaders;
206-
const url: Url = urlparse(headers[":path"] ?? "/");
207-
connectionId++;
208-
yield { connectionId, url, headers, stream };
209-
}
210-
}
211-
212238
async function respondDiscovery(
213239
response: ServiceDiscoveryResponse,
214240
http2Stream: http2.ServerHttp2Stream

test/testdriver.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
START_MESSAGE_TYPE,
1616
StartMessage,
1717
} from "../src/types/protocol";
18-
import * as restate from "../src/public_api";
1918
import { Connection } from "../src/connection/connection";
2019
import { printMessageAsJson } from "../src/utils/utils";
2120
import { Message } from "../src/types/types";
@@ -25,6 +24,7 @@ import { rlog } from "../src/utils/logger";
2524
import { StateMachine } from "../src/state_machine";
2625
import { InvocationBuilder } from "../src/invocation";
2726
import { protoMetadata } from "../src/generated/proto/test";
27+
import { BaseRestateServer } from "../src/server/base_restate_server";
2828

2929
export class TestDriver<I, O> implements Connection {
3030
private readonly result: Message[] = [];
@@ -182,7 +182,11 @@ export class TestDriver<I, O> implements Connection {
182182
* make it simpler for users to understand what methods are relevant for them,
183183
* and which ones are not.
184184
*/
185-
class TestRestateServer extends restate.RestateServer {
185+
class TestRestateServer extends BaseRestateServer {
186+
constructor() {
187+
super(ProtocolMode.BIDI_STREAM);
188+
}
189+
186190
public methodByUrl<I, O>(
187191
url: string | null | undefined
188192
): HostedGrpcServiceMethod<I, O> | undefined {

0 commit comments

Comments
 (0)