Skip to content

Commit 8e2e6e1

Browse files
committed
Vertx HTTP: execute custom logic when HTTP server is started
- resolves #42366
1 parent b3e6085 commit 8e2e6e1

File tree

6 files changed

+197
-11
lines changed

6 files changed

+197
-11
lines changed

docs/src/main/asciidoc/http-reference.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,27 @@ public class MyCustomizer implements HttpServerOptionsCustomizer {
498498
<1> By making the class a managed bean, Quarkus will take the customizer into account when it starts the Vert.x servers
499499
<2> In this case, we only care about customizing the HTTP server, so we just override the `customizeHttpServer` method, but users should be aware that `HttpServerOptionsCustomizer` allows configuring the HTTPS and Domain Socket servers as well
500500

501+
502+
== How to execute logic when HTTP server started
503+
504+
In order to execute some custom action when the HTTP server is started you'll need to declare an _asynchronous_ CDI observer method.
505+
Quarkus _asynchronously_ fires CDI events of types `io.quarkus.vertx.http.HttpServerStart`, `io.quarkus.vertx.http.HttpsServerStart` and `io.quarkus.vertx.http.DomainSocketServerStart` when the corresponding HTTP server starts listening on the configured host and port.
506+
507+
.`HttpServerStart` example
508+
[source,java]
509+
----
510+
@ApplicationScoped
511+
public class MyListener {
512+
513+
void httpStarted(@ObservesAsync HttpServerStart start) { <1>
514+
// ...notified when the HTTP server starts listening
515+
}
516+
}
517+
----
518+
<1> An asynchronous `HttpServerStart` observer method may be declared by annotating an `HttpServerStart` parameter with `@jakarta.enterprise.event.ObservesAsync`.
519+
520+
NOTE: It's not possible to use the `StartupEvent` for this particular use case because this CDI event is fired before the HTTP server is started.
521+
501522
[[reverse-proxy]]
502523
== Running behind a reverse proxy
503524

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.quarkus.vertx.http.start;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.io.File;
8+
import java.util.concurrent.CountDownLatch;
9+
import java.util.concurrent.TimeUnit;
10+
import java.util.concurrent.atomic.AtomicInteger;
11+
12+
import jakarta.annotation.PreDestroy;
13+
import jakarta.enterprise.context.Dependent;
14+
import jakarta.enterprise.event.ObservesAsync;
15+
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.RegisterExtension;
18+
19+
import io.quarkus.test.QuarkusUnitTest;
20+
import io.quarkus.vertx.http.HttpServerStart;
21+
import io.quarkus.vertx.http.HttpsServerStart;
22+
import io.smallrye.certs.Format;
23+
import io.smallrye.certs.junit5.Certificate;
24+
import io.smallrye.certs.junit5.Certificates;
25+
26+
@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "ssl-test", password = "secret", formats = {
27+
Format.JKS, Format.PKCS12, Format.PEM }))
28+
public class HttpServerStartEventsTest {
29+
30+
@RegisterExtension
31+
static final QuarkusUnitTest config = new QuarkusUnitTest()
32+
.withApplicationRoot(root -> root.addClasses(MyListener.class)
33+
.addAsResource(new File("target/certs/ssl-test-keystore.jks"), "server-keystore.jks"))
34+
.overrideConfigKey("quarkus.http.ssl.certificate.key-store-file", "server-keystore.jks")
35+
.overrideConfigKey("quarkus.http.ssl.certificate.key-store-password", "secret");
36+
37+
@Test
38+
public void test() throws InterruptedException {
39+
assertTrue(MyListener.HTTP.await(5, TimeUnit.SECONDS));
40+
assertTrue(MyListener.HTTPS.await(5, TimeUnit.SECONDS));
41+
// httpsStarted() is static
42+
assertEquals(1, MyListener.COUNTER.get());
43+
}
44+
45+
@Dependent
46+
public static class MyListener {
47+
48+
static final AtomicInteger COUNTER = new AtomicInteger();
49+
static final CountDownLatch HTTP = new CountDownLatch(1);
50+
static final CountDownLatch HTTPS = new CountDownLatch(1);
51+
52+
void httpStarted(@ObservesAsync HttpServerStart start) {
53+
assertNotNull(start.options());
54+
HTTP.countDown();
55+
}
56+
57+
static void httpsStarted(@ObservesAsync HttpsServerStart start) {
58+
assertNotNull(start.options());
59+
HTTPS.countDown();
60+
}
61+
62+
@PreDestroy
63+
void destroy() {
64+
COUNTER.incrementAndGet();
65+
}
66+
67+
}
68+
69+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.quarkus.vertx.http;
2+
3+
import io.vertx.core.http.HttpServerOptions;
4+
5+
/**
6+
* Quarkus fires a CDI event of this type asynchronously when the domain socket server starts listening
7+
* on the configured host and port.
8+
*
9+
* <pre>
10+
* &#064;ApplicationScoped
11+
* public class MyListener {
12+
*
13+
* void domainSocketStarted(&#064;ObservesAsync DomainSocketServerStart start) {
14+
* // ...notified when the domain socket server starts listening
15+
* }
16+
* }
17+
* </pre>
18+
*/
19+
public record DomainSocketServerStart(HttpServerOptions options) {
20+
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.quarkus.vertx.http;
2+
3+
import io.vertx.core.http.HttpServerOptions;
4+
5+
/**
6+
* Quarkus fires a CDI event of this type asynchronously when the HTTP server starts listening
7+
* on the configured host and port.
8+
*
9+
* <pre>
10+
* &#064;ApplicationScoped
11+
* public class MyListener {
12+
*
13+
* void httpStarted(&#064;ObservesAsync HttpServerStart start) {
14+
* // ...notified when the HTTP server starts listening
15+
* }
16+
* }
17+
* </pre>
18+
*/
19+
public record HttpServerStart(HttpServerOptions options) {
20+
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.quarkus.vertx.http;
2+
3+
import io.vertx.core.http.HttpServerOptions;
4+
5+
/**
6+
* Quarkus fires a CDI event of this type asynchronously when the HTTPS server starts listening
7+
* on the configured host and port.
8+
*
9+
* <pre>
10+
* &#064;ApplicationScoped
11+
* public class MyListener {
12+
*
13+
* void httpsStarted(&#064;ObservesAsync HttpsServerStart start) {
14+
* // ...notified when the HTTPS server starts listening
15+
* }
16+
* }
17+
* </pre>
18+
*/
19+
public record HttpsServerStart(HttpServerOptions options) {
20+
21+
}

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.concurrent.ExecutionException;
2424
import java.util.concurrent.Executor;
2525
import java.util.concurrent.RejectedExecutionException;
26+
import java.util.concurrent.atomic.AtomicBoolean;
2627
import java.util.concurrent.atomic.AtomicInteger;
2728
import java.util.function.BiConsumer;
2829
import java.util.function.BiFunction;
@@ -46,6 +47,7 @@
4647
import io.netty.handler.codec.http.HttpHeaderNames;
4748
import io.netty.handler.codec.http.HttpResponseStatus;
4849
import io.quarkus.arc.Arc;
50+
import io.quarkus.arc.ArcContainer;
4951
import io.quarkus.arc.InstanceHandle;
5052
import io.quarkus.arc.runtime.BeanContainer;
5153
import io.quarkus.bootstrap.runner.Timing;
@@ -71,7 +73,10 @@
7173
import io.quarkus.tls.runtime.config.TlsConfig;
7274
import io.quarkus.vertx.core.runtime.VertxCoreRecorder;
7375
import io.quarkus.vertx.core.runtime.config.VertxConfiguration;
76+
import io.quarkus.vertx.http.DomainSocketServerStart;
7477
import io.quarkus.vertx.http.HttpServerOptionsCustomizer;
78+
import io.quarkus.vertx.http.HttpServerStart;
79+
import io.quarkus.vertx.http.HttpsServerStart;
7580
import io.quarkus.vertx.http.ManagementInterface;
7681
import io.quarkus.vertx.http.runtime.HttpConfiguration.InsecureRequests;
7782
import io.quarkus.vertx.http.runtime.devmode.RemoteSyncHandler;
@@ -744,8 +749,9 @@ private static CompletableFuture<String> initializeMainHttpServer(Vertx vertx, H
744749
launchMode, websocketSubProtocols, registry);
745750

746751
// Customize
747-
if (Arc.container() != null) {
748-
List<InstanceHandle<HttpServerOptionsCustomizer>> instances = Arc.container()
752+
ArcContainer container = Arc.container();
753+
if (container != null) {
754+
List<InstanceHandle<HttpServerOptionsCustomizer>> instances = container
749755
.listAll(HttpServerOptionsCustomizer.class);
750756
for (InstanceHandle<HttpServerOptionsCustomizer> instance : instances) {
751757
HttpServerOptionsCustomizer customizer = instance.get();
@@ -784,12 +790,17 @@ private static CompletableFuture<String> initializeMainHttpServer(Vertx vertx, H
784790
CompletableFuture<String> futureResult = new CompletableFuture<>();
785791

786792
AtomicInteger connectionCount = new AtomicInteger();
793+
794+
// Note that a new HttpServer is created for each IO thread but we only want to fire the events (HttpServerStart etc.) once,
795+
// for the first server that started listening
796+
// See https://vertx.io/docs/vertx-core/java/#_server_sharing for more information
797+
AtomicBoolean startEventsFired = new AtomicBoolean();
798+
787799
vertx.deployVerticle(new Supplier<Verticle>() {
788800
@Override
789801
public Verticle get() {
790802
return new WebDeploymentVerticle(httpMainServerOptions, httpMainSslServerOptions, httpMainDomainSocketOptions,
791-
launchMode,
792-
insecureRequestStrategy, httpConfiguration, connectionCount, registry);
803+
launchMode, insecureRequestStrategy, httpConfiguration, connectionCount, registry, startEventsFired);
793804
}
794805
}, new DeploymentOptions().setInstances(ioThreads), new Handler<AsyncResult<String>>() {
795806
@Override
@@ -1129,11 +1140,12 @@ private static class WebDeploymentVerticle extends AbstractVerticle implements R
11291140
private final HttpConfiguration quarkusConfig;
11301141
private final AtomicInteger connectionCount;
11311142
private final List<Long> reloadingTasks = new CopyOnWriteArrayList<>();
1143+
private final AtomicBoolean startEventsFired;
11321144

11331145
public WebDeploymentVerticle(HttpServerOptions httpOptions, HttpServerOptions httpsOptions,
11341146
HttpServerOptions domainSocketOptions, LaunchMode launchMode,
11351147
InsecureRequests insecureRequests, HttpConfiguration quarkusConfig, AtomicInteger connectionCount,
1136-
TlsConfigurationRegistry registry) {
1148+
TlsConfigurationRegistry registry, AtomicBoolean startEventsFired) {
11371149
this.httpOptions = httpOptions;
11381150
this.httpsOptions = httpsOptions;
11391151
this.launchMode = launchMode;
@@ -1142,6 +1154,7 @@ public WebDeploymentVerticle(HttpServerOptions httpOptions, HttpServerOptions ht
11421154
this.quarkusConfig = quarkusConfig;
11431155
this.connectionCount = connectionCount;
11441156
this.registry = registry;
1157+
this.startEventsFired = startEventsFired;
11451158
org.crac.Core.getGlobalContext().register(this);
11461159
}
11471160

@@ -1166,6 +1179,9 @@ public void start(Promise<Void> startFuture) {
11661179
.fail(new IllegalArgumentException("Must configure at least one of http, https or unix domain socket"));
11671180
}
11681181

1182+
ArcContainer container = Arc.container();
1183+
boolean notifyStartObservers = container != null ? startEventsFired.compareAndSet(false, true) : false;
1184+
11691185
if (httpServerEnabled) {
11701186
httpServer = vertx.createHttpServer(httpOptions);
11711187
if (insecureRequests == HttpConfiguration.InsecureRequests.ENABLED) {
@@ -1196,27 +1212,34 @@ public void handle(HttpServerRequest req) {
11961212
}
11971213
});
11981214
}
1199-
setupTcpHttpServer(httpServer, httpOptions, false, startFuture, remainingCount, connectionCount);
1215+
setupTcpHttpServer(httpServer, httpOptions, false, startFuture, remainingCount, connectionCount,
1216+
container, notifyStartObservers);
12001217
}
12011218

12021219
if (domainSocketOptions != null) {
12031220
domainSocketServer = vertx.createHttpServer(domainSocketOptions);
12041221
domainSocketServer.requestHandler(ACTUAL_ROOT);
1205-
setupUnixDomainSocketHttpServer(domainSocketServer, domainSocketOptions, startFuture, remainingCount);
1222+
setupUnixDomainSocketHttpServer(domainSocketServer, domainSocketOptions, startFuture, remainingCount,
1223+
container, notifyStartObservers);
12061224
}
12071225

12081226
if (httpsOptions != null) {
12091227
httpsServer = vertx.createHttpServer(httpsOptions);
12101228
httpsServer.requestHandler(ACTUAL_ROOT);
1211-
setupTcpHttpServer(httpsServer, httpsOptions, true, startFuture, remainingCount, connectionCount);
1229+
setupTcpHttpServer(httpsServer, httpsOptions, true, startFuture, remainingCount, connectionCount,
1230+
container, notifyStartObservers);
12121231
}
12131232
}
12141233

12151234
private void setupUnixDomainSocketHttpServer(HttpServer httpServer, HttpServerOptions options,
12161235
Promise<Void> startFuture,
1217-
AtomicInteger remainingCount) {
1236+
AtomicInteger remainingCount, ArcContainer container, boolean notifyStartObservers) {
12181237
httpServer.listen(SocketAddress.domainSocketAddress(options.getHost()), event -> {
12191238
if (event.succeeded()) {
1239+
if (notifyStartObservers) {
1240+
container.beanManager().getEvent().select(DomainSocketServerStart.class)
1241+
.fireAsync(new DomainSocketServerStart(options));
1242+
}
12201243
if (remainingCount.decrementAndGet() == 0) {
12211244
startFuture.complete(null);
12221245
}
@@ -1240,7 +1263,8 @@ private void setupUnixDomainSocketHttpServer(HttpServer httpServer, HttpServerOp
12401263
}
12411264

12421265
private void setupTcpHttpServer(HttpServer httpServer, HttpServerOptions options, boolean https,
1243-
Promise<Void> startFuture, AtomicInteger remainingCount, AtomicInteger currentConnectionCount) {
1266+
Promise<Void> startFuture, AtomicInteger remainingCount, AtomicInteger currentConnectionCount,
1267+
ArcContainer container, boolean notifyStartObservers) {
12441268

12451269
if (quarkusConfig.limits.maxConnections.isPresent() && quarkusConfig.limits.maxConnections.getAsInt() > 0) {
12461270
var tracker = vertx.isMetricsEnabled()
@@ -1315,11 +1339,20 @@ public void handle(AsyncResult<HttpServer> event) {
13151339
}
13161340

13171341
if (https) {
1318-
CDI.current().select(HttpCertificateUpdateEventListener.class).get()
1342+
container.instance(HttpCertificateUpdateEventListener.class).get()
13191343
.register(event.result(), quarkusConfig.tlsConfigurationName.orElse(TlsConfig.DEFAULT_NAME),
13201344
"http server");
13211345
}
13221346

1347+
if (notifyStartObservers) {
1348+
Event<Object> startEvent = container.beanManager().getEvent();
1349+
if (https) {
1350+
startEvent.select(HttpsServerStart.class).fireAsync(new HttpsServerStart(options));
1351+
} else {
1352+
startEvent.select(HttpServerStart.class).fireAsync(new HttpServerStart(options));
1353+
}
1354+
}
1355+
13231356
if (remainingCount.decrementAndGet() == 0) {
13241357
//make sure we only complete once
13251358
startFuture.complete(null);

0 commit comments

Comments
 (0)