Skip to content

Commit 152b712

Browse files
Add support for configurable event loops and enable multi-event-loop setup by default. (#630)
In spring this is configurable in the server properties as restate.sdk.http.eventLoops In vanilla SDK this is configurable using the env variable RESTATE_SDK_HTTP_EVENT_LOOPS or sys property dev.restate.sdk.http.eventLoops
1 parent 613231d commit 152b712

3 files changed

Lines changed: 172 additions & 35 deletions

File tree

sdk-http-vertx/src/main/java/dev/restate/sdk/http/vertx/RestateHttpServer.java

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
package dev.restate.sdk.http.vertx;
1010

1111
import dev.restate.sdk.endpoint.Endpoint;
12-
import io.vertx.core.Future;
12+
import io.vertx.core.AbstractVerticle;
13+
import io.vertx.core.DeploymentOptions;
14+
import io.vertx.core.Promise;
1315
import io.vertx.core.Vertx;
14-
import io.vertx.core.http.Http2Settings;
16+
import io.vertx.core.VertxOptions;
1517
import io.vertx.core.http.HttpServer;
1618
import io.vertx.core.http.HttpServerOptions;
1719
import java.util.Optional;
1820
import java.util.concurrent.CompletionException;
21+
import java.util.concurrent.atomic.AtomicInteger;
1922
import org.apache.logging.log4j.LogManager;
2023
import org.apache.logging.log4j.Logger;
2124

@@ -42,9 +45,8 @@ public class RestateHttpServer {
4245

4346
private static final int DEFAULT_PORT =
4447
Optional.ofNullable(System.getenv("PORT")).map(Integer::parseInt).orElse(9080);
45-
private static final HttpServerOptions DEFAULT_OPTIONS =
46-
new HttpServerOptions()
47-
.setInitialSettings(new Http2Settings().setMaxConcurrentStreams(Integer.MAX_VALUE));
48+
private static final HttpServerOptions DEFAULT_HTTP_SERVER_OPTIONS = new HttpServerOptions();
49+
private static final int DEFAULT_EVENT_LOOPS = VertxOptions.DEFAULT_EVENT_LOOP_POOL_SIZE;
4850

4951
/**
5052
* Start serving the provided {@code endpoint} on the port specified by the environment variable
@@ -54,10 +56,10 @@ public class RestateHttpServer {
5456
* non-blocking variant, manually create the server with {@link #fromEndpoint(Endpoint)} and start
5557
* listening it.
5658
*
57-
* @return The listening port
59+
* @return The listening port, use 0 for random port.
5860
*/
5961
public static int listen(Endpoint endpoint) {
60-
return handleStart(fromEndpoint(endpoint).listen(DEFAULT_PORT));
62+
return listen(endpoint, DEFAULT_PORT);
6163
}
6264

6365
/** Like {@link #listen(Endpoint)} */
@@ -72,10 +74,10 @@ public static int listen(Endpoint.Builder endpointBuilder) {
7274
* non-blocking variant, manually create the server with {@link #fromEndpoint(Endpoint)} and start
7375
* listening it.
7476
*
75-
* @return The listening port
77+
* @return The listening port, use 0 for random port.
7678
*/
7779
public static int listen(Endpoint endpoint, int port) {
78-
return handleStart(fromEndpoint(endpoint).listen(port));
80+
return listen(HttpEndpointRequestHandler.fromEndpoint(endpoint), port);
7981
}
8082

8183
/** Like {@link #listen(Endpoint, int)} */
@@ -90,12 +92,12 @@ public static int listen(HttpEndpointRequestHandler requestHandler) {
9092

9193
/** Like {@link #listen(Endpoint, int)}, with an already built request handler */
9294
public static int listen(HttpEndpointRequestHandler requestHandler, int port) {
93-
return handleStart(fromHandler(requestHandler).listen(port));
95+
return listenBlocking(requestHandler, port);
9496
}
9597

9698
/** Create a Vert.x {@link HttpServer} from the provided endpoint. */
9799
public static HttpServer fromEndpoint(Endpoint endpoint) {
98-
return fromEndpoint(endpoint, DEFAULT_OPTIONS);
100+
return fromEndpoint(endpoint, DEFAULT_HTTP_SERVER_OPTIONS);
99101
}
100102

101103
/** Like {@link #fromEndpoint(Endpoint)} */
@@ -119,7 +121,7 @@ public static HttpServer fromEndpoint(
119121

120122
/** Create a Vert.x {@link HttpServer} from the provided endpoint. */
121123
public static HttpServer fromEndpoint(Vertx vertx, Endpoint endpoint) {
122-
return fromEndpoint(vertx, endpoint, DEFAULT_OPTIONS);
124+
return fromEndpoint(vertx, endpoint, DEFAULT_HTTP_SERVER_OPTIONS);
123125
}
124126

125127
/** Like {@link #fromEndpoint(Vertx, Endpoint)} */
@@ -143,7 +145,7 @@ public static HttpServer fromEndpoint(
143145

144146
/** Create a Vert.x {@link HttpServer} from the provided {@link HttpEndpointRequestHandler}. */
145147
public static HttpServer fromHandler(HttpEndpointRequestHandler handler) {
146-
return fromHandler(handler, DEFAULT_OPTIONS);
148+
return fromHandler(handler, DEFAULT_HTTP_SERVER_OPTIONS);
147149
}
148150

149151
/**
@@ -157,7 +159,7 @@ public static HttpServer fromHandler(
157159

158160
/** Create a Vert.x {@link HttpServer} from the provided {@link HttpEndpointRequestHandler}. */
159161
public static HttpServer fromHandler(Vertx vertx, HttpEndpointRequestHandler handler) {
160-
return fromHandler(vertx, handler, DEFAULT_OPTIONS);
162+
return fromHandler(vertx, handler, DEFAULT_HTTP_SERVER_OPTIONS);
161163
}
162164

163165
/**
@@ -171,19 +173,74 @@ public static HttpServer fromHandler(
171173
return server;
172174
}
173175

174-
private static int handleStart(Future<HttpServer> fut) {
176+
private static int listenBlocking(HttpEndpointRequestHandler handler, int port) {
177+
String eventLoopsOverride =
178+
System.getProperty(
179+
"dev.restate.sdk.http.eventLoops", System.getenv("RESTATE_SDK_HTTP_EVENT_LOOPS"));
180+
int instances =
181+
eventLoopsOverride != null ? Integer.parseInt(eventLoopsOverride) : DEFAULT_EVENT_LOOPS;
182+
Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(instances));
183+
// A negative port makes all the server instances share the same random port, whereas port 0
184+
// would make each instance bind a different random port.
185+
int listenPort = port == 0 ? -1 : port;
186+
AtomicInteger actualPort = new AtomicInteger();
175187
try {
176-
HttpServer server = fut.toCompletionStage().toCompletableFuture().join();
177-
LOG.info("Restate HTTP Endpoint server started on port {}", server.actualPort());
178-
return server.actualPort();
188+
vertx
189+
.deployVerticle(
190+
() ->
191+
new RestateServerVerticle(
192+
handler, DEFAULT_HTTP_SERVER_OPTIONS, listenPort, actualPort),
193+
new DeploymentOptions().setInstances(instances))
194+
.toCompletionStage()
195+
.toCompletableFuture()
196+
.join();
197+
LOG.info(
198+
"Restate HTTP Endpoint server started on port {} with {} event loop(s)",
199+
actualPort.get(),
200+
instances);
201+
return actualPort.get();
179202
} catch (CompletionException e) {
180203
LOG.error("Restate HTTP Endpoint server start failed", e.getCause());
204+
vertx.close();
181205
sneakyThrow(e.getCause());
182206
// This is never reached
183207
return -1;
184208
}
185209
}
186210

211+
private static final class RestateServerVerticle extends AbstractVerticle {
212+
213+
private final HttpEndpointRequestHandler handler;
214+
private final HttpServerOptions options;
215+
private final int port;
216+
private final AtomicInteger actualPort;
217+
218+
private RestateServerVerticle(
219+
HttpEndpointRequestHandler handler,
220+
HttpServerOptions options,
221+
int port,
222+
AtomicInteger actualPort) {
223+
this.handler = handler;
224+
this.options = options;
225+
this.port = port;
226+
this.actualPort = actualPort;
227+
}
228+
229+
@Override
230+
public void start(Promise<Void> startPromise) {
231+
HttpServer server = vertx.createHttpServer(options);
232+
server.requestHandler(handler);
233+
server
234+
.listen(port)
235+
.onSuccess(
236+
s -> {
237+
actualPort.set(s.actualPort());
238+
startPromise.complete();
239+
})
240+
.onFailure(startPromise::fail);
241+
}
242+
}
243+
187244
@SuppressWarnings("unchecked")
188245
private static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
189246
throw (E) e;

sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpEndpointBean.java

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
import dev.restate.sdk.endpoint.Endpoint;
1313
import dev.restate.sdk.http.vertx.HttpEndpointRequestHandler;
1414
import dev.restate.sdk.http.vertx.RestateHttpServer;
15-
import io.vertx.core.http.HttpServer;
15+
import io.vertx.core.AbstractVerticle;
16+
import io.vertx.core.DeploymentOptions;
17+
import io.vertx.core.Promise;
18+
import io.vertx.core.Vertx;
19+
import io.vertx.core.VertxOptions;
1620
import java.util.Map;
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
import org.jspecify.annotations.Nullable;
1723
import org.slf4j.Logger;
1824
import org.slf4j.LoggerFactory;
1925
import org.springframework.beans.factory.InitializingBean;
@@ -29,15 +35,18 @@ public class RestateHttpEndpointBean implements InitializingBean, SmartLifecycle
2935

3036
private volatile boolean running;
3137

32-
private final HttpServer server;
38+
private final @Nullable HttpEndpointRequestHandler handler;
39+
40+
private volatile @Nullable Vertx vertx;
41+
42+
private volatile int actualPort = -1;
3343

3444
public RestateHttpEndpointBean(
3545
Endpoint endpoint, RestateHttpServerProperties restateHttpServerProperties) {
3646
this.restateHttpServerProperties = restateHttpServerProperties;
37-
this.server =
38-
RestateHttpServer.fromHandler(
39-
HttpEndpointRequestHandler.fromEndpoint(
40-
endpoint, this.restateHttpServerProperties.isDisableBidirectionalStreaming()));
47+
this.handler =
48+
HttpEndpointRequestHandler.fromEndpoint(
49+
endpoint, restateHttpServerProperties.isDisableBidirectionalStreaming());
4150
}
4251

4352
@Deprecated
@@ -52,7 +61,7 @@ public RestateHttpEndpointBean(
5261

5362
if (restateComponents.isEmpty()) {
5463
logger.info("No @RestateComponent discovered");
55-
this.server = null;
64+
this.handler = null;
5665
// Don't start anything, if no service is registered
5766
return;
5867
}
@@ -80,10 +89,10 @@ public RestateHttpEndpointBean(
8089
RestateRequestIdentityVerifier.fromKey(restateEndpointProperties.getIdentityKey()));
8190
}
8291

83-
this.server =
84-
RestateHttpServer.fromHandler(
85-
HttpEndpointRequestHandler.fromEndpoint(
86-
builder.build(), restateHttpServerProperties.isDisableBidirectionalStreaming()));
92+
Endpoint endpoint = builder.build();
93+
this.handler =
94+
HttpEndpointRequestHandler.fromEndpoint(
95+
endpoint, restateHttpServerProperties.isDisableBidirectionalStreaming());
8796
}
8897

8998
@Deprecated
@@ -92,13 +101,39 @@ public void afterPropertiesSet() {}
92101

93102
@Override
94103
public void start() {
104+
HttpEndpointRequestHandler handler = this.handler;
105+
if (handler == null) {
106+
// No service registered, nothing to start.
107+
this.running = true;
108+
return;
109+
}
110+
int instances =
111+
this.restateHttpServerProperties.getEventLoops() > 0
112+
? this.restateHttpServerProperties.getEventLoops()
113+
: VertxOptions.DEFAULT_EVENT_LOOP_POOL_SIZE;
114+
Vertx vertx = Vertx.vertx(new VertxOptions().setEventLoopPoolSize(instances));
115+
this.vertx = vertx;
116+
// A negative port makes all the server instances share the same random port, whereas port 0
117+
// would make each instance bind a different random port.
118+
int listenPort =
119+
this.restateHttpServerProperties.getPort() == 0
120+
? -1
121+
: this.restateHttpServerProperties.getPort();
122+
AtomicInteger actualPortHolder = new AtomicInteger();
95123
try {
96-
this.server
97-
.listen(this.restateHttpServerProperties.getPort())
124+
// Deploy one HttpServer (and one handler) per event loop, spreading connections across them.
125+
vertx
126+
.deployVerticle(
127+
() -> new ServerVerticle(handler, listenPort, actualPortHolder),
128+
new DeploymentOptions().setInstances(instances))
98129
.toCompletionStage()
99130
.toCompletableFuture()
100131
.get();
101-
logger.info("Started Restate Spring HTTP server on port {}", this.server.actualPort());
132+
this.actualPort = actualPortHolder.get();
133+
logger.info(
134+
"Started Restate Spring HTTP server on port {} with {} event loop(s)",
135+
this.actualPort,
136+
instances);
102137
} catch (Exception e) {
103138
logger.error(
104139
"Error when starting Restate Spring HTTP server on port {}",
@@ -111,11 +146,16 @@ public void start() {
111146
@Override
112147
public void stop() {
113148
try {
114-
this.server.close().toCompletionStage().toCompletableFuture().get();
149+
Vertx vertx = this.vertx;
150+
if (vertx != null) {
151+
vertx.close().toCompletionStage().toCompletableFuture().get();
152+
this.vertx = null;
153+
}
115154
logger.info("Stopped Restate Spring HTTP server");
116155
} catch (Exception e) {
117156
logger.error("Error when stopping the Restate Spring HTTP server", e);
118157
}
158+
this.actualPort = -1;
119159
this.running = false;
120160
}
121161

@@ -128,6 +168,35 @@ public int actualPort() {
128168
if (!this.isRunning()) {
129169
return -1;
130170
}
131-
return this.server.actualPort();
171+
return this.actualPort;
172+
}
173+
174+
/**
175+
* Verticle deployed once per event loop, each creating its own {@link
176+
* HttpEndpointRequestHandler}.
177+
*/
178+
private static final class ServerVerticle extends AbstractVerticle {
179+
180+
private final HttpEndpointRequestHandler handler;
181+
private final int port;
182+
private final AtomicInteger actualPort;
183+
184+
private ServerVerticle(HttpEndpointRequestHandler handler, int port, AtomicInteger actualPort) {
185+
this.handler = handler;
186+
this.port = port;
187+
this.actualPort = actualPort;
188+
}
189+
190+
@Override
191+
public void start(Promise<Void> startPromise) {
192+
RestateHttpServer.fromHandler(vertx, handler)
193+
.listen(port)
194+
.onSuccess(
195+
server -> {
196+
actualPort.set(server.actualPort());
197+
startPromise.complete();
198+
})
199+
.onFailure(startPromise::fail);
200+
}
132201
}
133202
}

sdk-spring-boot/src/main/java/dev/restate/sdk/springboot/RestateHttpServerProperties.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,32 @@ public class RestateHttpServerProperties {
1818

1919
private final int port;
2020
private final boolean disableBidirectionalStreaming;
21+
private final int eventLoops;
2122

2223
@ConstructorBinding
2324
public RestateHttpServerProperties(
2425
@Name("port") @DefaultValue(value = "9080") int port,
2526
@Name("disableBidirectionalStreaming") @DefaultValue(value = "false")
26-
boolean disableBidirectionalStreaming) {
27+
boolean disableBidirectionalStreaming,
28+
@Name("eventLoops") @DefaultValue(value = "0") int eventLoops) {
2729
this.port = port;
2830
this.disableBidirectionalStreaming = disableBidirectionalStreaming;
31+
this.eventLoops = eventLoops;
2932
}
3033

3134
/** Port to expose the HTTP server. */
3235
public int getPort() {
3336
return port;
3437
}
3538

39+
/**
40+
* Number of event loops to run, spreading incoming connections across that many CPU cores. When
41+
* {@code 0}, the default {@code 2 * availableProcessors} is used.
42+
*/
43+
public int getEventLoops() {
44+
return eventLoops;
45+
}
46+
3647
/**
3748
* If true, disable bidirectional streaming with HTTP/2 requests. Restate initiates for each
3849
* invocation a bidirectional streaming using HTTP/2 between restate-server and the SDK. In some

0 commit comments

Comments
 (0)