diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index e1b284c5a8587..10a20945120bd 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -163,7 +163,7 @@
6.0.04.7.11.5.2
- 0.33.10
+ 0.34.03.24.23.14.91.17.2
@@ -201,7 +201,7 @@
2.60.10.0
- 9.24.4
+ 9.250.0.60.1.1
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java
index 0a3f419a390b8..4d2d143e22dbf 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java
@@ -55,7 +55,6 @@
public class LoggingSetupRecorder {
private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LoggingSetupRecorder.class);
- public static final String SHUTDOWN_MESSAGE = " [Error Occurred After Shutdown]";
final RuntimeValue consoleRuntimeConfig;
diff --git a/docs/src/main/asciidoc/images/security-bearer-token-authorization-mechanism-1.png b/docs/src/main/asciidoc/images/security-bearer-token-authorization-mechanism-1.png
new file mode 100644
index 0000000000000..13271519264dc
Binary files /dev/null and b/docs/src/main/asciidoc/images/security-bearer-token-authorization-mechanism-1.png differ
diff --git a/docs/src/main/asciidoc/images/security-bearer-token-authorization-mechanism-2.png b/docs/src/main/asciidoc/images/security-bearer-token-authorization-mechanism-2.png
new file mode 100644
index 0000000000000..ea8cfdeed41d4
Binary files /dev/null and b/docs/src/main/asciidoc/images/security-bearer-token-authorization-mechanism-2.png differ
diff --git a/docs/src/main/asciidoc/mailer-reference.adoc b/docs/src/main/asciidoc/mailer-reference.adoc
index 67dff0bdfffcf..2aa9e6342d6f6 100644
--- a/docs/src/main/asciidoc/mailer-reference.adoc
+++ b/docs/src/main/asciidoc/mailer-reference.adoc
@@ -368,6 +368,8 @@ quarkus.mailer.port=587
quarkus.mailer.start-tls=REQUIRED
quarkus.mailer.username=YOUREMAIL@gmail.com
quarkus.mailer.password=YOURGENERATEDAPPLICATIONPASSWORD
+
+quarkus.mailer.mock=false # In dev mode, prevent from using the mock SMTP server
----
Or with SSL:
@@ -381,6 +383,8 @@ quarkus.mailer.port=465
quarkus.mailer.ssl=true
quarkus.mailer.username=YOUREMAIL@gmail.com
quarkus.mailer.password=YOURGENERATEDAPPLICATIONPASSWORD
+
+quarkus.mailer.mock=false # In dev mode, prevent from using the mock SMTP server
----
[NOTE]
diff --git a/docs/src/main/asciidoc/mailer.adoc b/docs/src/main/asciidoc/mailer.adoc
index 882d096f1bcd9..e85271a91c4fa 100644
--- a/docs/src/main/asciidoc/mailer.adoc
+++ b/docs/src/main/asciidoc/mailer.adoc
@@ -181,32 +181,10 @@ In the `src/main/resources/application.properties` file, you need to configure t
Note that the password can also be configured using system properties and environment variables.
See the xref:config-reference.adoc[configuration reference guide] for details.
-Here is an example using _sendgrid_:
-
-[source,properties]
-----
-# Your email address you send from - must match the "from" address from sendgrid.
-quarkus.mailer.from=test@quarkus.io
-
-# The SMTP host
-quarkus.mailer.host=smtp.sendgrid.net
-# The SMTP port
-quarkus.mailer.port=465
-# If the SMTP connection requires SSL/TLS
-quarkus.mailer.ssl=true
-# Your username
-quarkus.mailer.username=....
-# Your password
-quarkus.mailer.password=....
-
-# By default, in dev mode, the mailer is a mock. This disables the mock and use the configured mailer.
-quarkus.mailer.mock=false
-----
+Configuration of popular mail services is covered in xref:mailer-reference.adoc#popular[the reference guide].
Once you have configured the mailer, if you call the HTTP endpoint as shown above, you will send emails.
-Other popular mail services are covered in xref:mailer-reference.adoc#popular[the reference guide].
-
== Conclusion
This guide has shown how to send emails from your Quarkus application.
diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc
index d3130db9ecb9c..456090e604ee2 100644
--- a/docs/src/main/asciidoc/security-customization.adoc
+++ b/docs/src/main/asciidoc/security-customization.adoc
@@ -84,6 +84,8 @@ In some cases such a default logic of selecting the challenge is exactly what is
[source,java]
----
+@Alternative <1>
+@Priority(1)
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {
@@ -102,18 +104,21 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism
@Override
public Uni getChallenge(RoutingContext context) {
- return selectBetweenJwtAndOidcChallenge(context).getChallenge(context);
+ return selectBetweenJwtAndOidcChallenge(context).getChallenge(context);
}
@Override
public Set> getCredentialTypes() {
- return selectBetweenJwtAndOidc(context).getCredentialTypes();
+ Set> credentialTypes = new HashSet<>();
+ credentialTypes.addAll(jwt.getCredentialTypes());
+ credentialTypes.addAll(oidc.getCredentialTypes());
+ return credentialTypes;
}
- @Override
- public HttpCredentialTransport getCredentialTransport(RoutingContext context) {
- return selectBetweenJwtAndOidc(context).getCredentialTransport();
- }
+ @Override
+ public Uni getCredentialTransport(RoutingContext context) {
+ return selectBetweenJwtAndOidc(context).getCredentialTransport(context);
+ }
private HttpAuthenticationMechanism selectBetweenJwtAndOidc(RoutingContext context) {
....
@@ -125,6 +130,7 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism
}
----
+<1> Declaring the mechanism an alternative bean ensures this mechanism is used rather than `OidcAuthenticationMechanism` and `JWTAuthMechanism`.
[[security-identity-customization]]
== Security Identity Customization
diff --git a/docs/src/main/asciidoc/security-openid-connect.adoc b/docs/src/main/asciidoc/security-openid-connect.adoc
index 0b03187201eb9..af0f39765b706 100644
--- a/docs/src/main/asciidoc/security-openid-connect.adoc
+++ b/docs/src/main/asciidoc/security-openid-connect.adoc
@@ -8,15 +8,37 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
include::./attributes.adoc[]
:toc:
-This guide demonstrates how to use Quarkus OpenID Connect (OIDC) Extension to protect your JAX-RS applications using Bearer Token Authorization where Bearer Tokens are issued by OpenID Connect and OAuth 2.0 compliant Authorization Servers such as https://www.keycloak.org[Keycloak].
+You can use the Quarkus OpenID Connect (OIDC) extension to secure your JAX-RS applications using Bearer Token Authorization.
+The Bearer Tokens are issued by OIDC and OAuth 2.0 compliant authorization servers, such as https://www.keycloak.org[Keycloak].
-Bearer Token Authorization is the process of authorizing HTTP requests based on the existence and validity of a Bearer Token which provides valuable information to determine the subject of the call as well as whether an HTTP resource can be accessed.
+Bearer Token Authorization is the process of authorizing HTTP requests based on the existence and validity of a Bearer Token.
+The Bearer Token provides information about the subject of the call which is used to determine whether or not an HTTP resource can be accessed.
-Please read the xref:security-openid-connect-web-authentication.adoc[Using OpenID Connect to Protect Web Applications] guide if you need to authenticate and authorize the users using OpenID Connect Authorization Code Flow.
+The following diagrams outline the Bearer Token Authorization mechanism in Quarkus:
-If you use Keycloak and Bearer tokens then also see the xref:security-keycloak-authorization.adoc[Using Keycloak to Centralize Authorization] guide.
+.Bearer Token Authorization mechanism in Quarkus with Single-page application
+image::security-bearer-token-authorization-mechanism-1.png[alt=Bearer Token Authorization, width="60%", align=center]
+
+1. The Quarkus service retrieves verification keys from the OpenID Connect provider. The verification keys are used to verify the bearer access token signatures.
+2. The Quarkus user accesses the Single-page application.
+3. The Single-page application uses Authorization Code Flow to authenticate the user and retrieve tokens from the OpenID Connect provider.
+4. The Single-page application uses the access token to retrieve the service data from the Quarkus service.
+5. The Quarkus service verifies the bearer access token signature using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the Single-page application.
+6. The Single-page application returns the same data to the Quarkus user.
+
+.Bearer Token Authorization mechanism in Quarkus with Java or command line client
+image::security-bearer-token-authorization-mechanism-2.png[alt=Bearer Token Authorization, width="60%", align=center]
+
+1. The Quarkus service retrieves verification keys from the OpenID Connect provider. The verification keys are used to verify the bearer access token signatures.
+2. The Client uses `client_credentials` that requires client ID and secret or password grant, which also requires client ID, secret, user name, and password to retrieve the access token from the OpenID Connect provider.
+3. The Client uses the access token to retrieve the service data from the Quarkus service.
+4. The Quarkus service verifies the bearer access token signature using the verification keys, checks the token expiry date and other claims, allows the request to proceed if the token is valid, and returns the service response to the Client.
+
+If you need to authenticate and authorize the users using OpenID Connect Authorization Code Flow, see xref:security-openid-connect-web-authentication.adoc[Using OpenID Connect to Protect Web Applications].
+Also, if you use Keycloak and Bearer Tokens, see xref:security-keycloak-authorization.adoc[Using Keycloak to Centralize Authorization].
+
+For information about how to support multiple tenants, see xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy].
-Please read the xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy] guide how to support multiple tenants.
== Quickstart
diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc
new file mode 100644
index 0000000000000..e55f24476308e
--- /dev/null
+++ b/docs/src/main/asciidoc/virtual-threads.adoc
@@ -0,0 +1,521 @@
+////
+This guide is maintained in the main Quarkus repository
+and pull requests should be submitted there:
+https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
+////
+= Writing simpler reactive REST services with Quarkus Virtual Thread support
+
+include::./attributes.adoc[]
+:resteasy-reactive-api: https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive/{quarkus-version}
+:resteasy-reactive-common-api: https://javadoc.io/doc/io.quarkus.resteasy.reactive/resteasy-reactive-common/{quarkus-version}
+:runonvthread: https://javadoc.io/doc/io.smallrye.common/smallrye-common-annotation/latest/io/smallrye/common/annotation/RunOnVirtualThread.html
+:blockingannotation: https://javadoc.io/doc/io.smallrye.common/smallrye-common-annotation/latest/io/smallrye/common/annotation/Blocking.html
+:vthreadjep: https://openjdk.org/jeps/425
+:thread: https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/Thread.html
+:mutiny-vertx-sql: https://smallrye.io/smallrye-mutiny-vertx-bindings/2.26.0/apidocs/io/vertx/mutiny/sqlclient/package-summary.html
+:pgsql-driver: https://javadoc.io/doc/org.postgresql/postgresql/latest/index.html
+
+This guide explains how to benefit from Java 19 virtual threads when writing REST services in Quarkus.
+
+[TIP]
+====
+This is the reference guide for using virtual threads to write reactive REST services.
+Please refer to the xref:rest-json.adoc[Writing JSON REST services guides] for a lightweight introduction to reactive REST
+services and to the xref:resteasy-reactive.adoc[Writing REST Services with RESTEasy Reactive] guide for a detailed presentation.
+====
+
+== What are virtual threads ?
+
+=== Terminology
+OS thread::
+A "thread-like" data-structure managed by the Operating System.
+
+Platform thread::
+Up until Java 19, every instance of the link:{thread}[Thread] class was a platform thread, that is, a wrapper around an OS thread.
+Creating a platform threads creates an OS thread, blocking a platform thread blocks an OS thread.
+
+Virtual thread::
+Lightweight, JVM-managed threads. They extend the link:{thread}[Thread] class but are not tied to one specific OS thread.
+Thus, scheduling virtual threads is the responsibility of the JVM.
+
+Carrier thread::
+A platform thread used to execute a virtual thread is called a carrier.
+This isn't a class distinct from link:{Thread}[Thread] or VirtualThread but rather a functional denomination.
+
+=== Differences between virtual threads and platform threads
+We will give a brief overview of the topic here, please refer to the link:{vthreadjep}[JEP 425] for more information.
+
+Virtual threads are a feature available since Java 19 aiming at providing a cheap alternative to platform threads for I/O-bound workloads.
+
+Until now, platform threads were the concurrency unit of the JVM.
+They are a wrapper over OS structures.
+This means that creating a Java platform thread actually results in creating a "thread-like" structure in your operating system.
+
+Virtual threads on the other hand are managed by the JVM. In order to be executed, they need to be mounted on a platform thread
+(which acts as a carrier to that virtual thread).
+As such, they have been designed to offer the following characteristics:
+
+Lightweight :: Virtual threads occupy less space than platform threads in memory.
+Hence, it becomes possible to use more virtual threads than platform threads simultaneously without blowing up the heap.
+By default, platform threads are created with a stack of about 1 MB where virtual threads stack is "pay-as-you-go".
+You can find these numbers along with other motivations for virtual threads in this presentation given by the lead developer of project Loom: https://youtu.be/lIq-x_iI-kc?t=543.
+
+Cheap to create:: Creating a platform thread in Java takes time.
+Currently, techniques such as pooling where threads are created once then reused are strongly encouraged to minimize the
+time lost in starting them (as well as limiting the maximum number of threads to keep memory consumption low).
+Virtual threads are supposed to be disposable entities that we create when we need them,
+it is discouraged to pool them or to reuse them for different tasks.
+
+Cheap to block:: When performing blocking I/O, the underlying OS thread wrapped by the Java platform thread is put in a
+wait queue and a context switch occurs to load a new thread context onto the CPU core. This operation takes time.
+Since virtual threads are managed by the JVM, no underlying OS thread is blocked when they perform a blocking operation.
+Their state is simply stored in the heap and another Virtual thread is executed on the same Java platform thread.
+
+=== Virtual threads are useful for I/O-bound workloads only
+We now know that we can create way more virtual threads than platform threads. One could be tempted to use virtual threads
+to perform long computations (CPU-bound workload).
+This is useless if not counterproductive.
+CPU-bound doesn't consist in quickly swapping threads while they need to wait for the completion of an I/O but in leaving
+them attached to a CPU-core to actually compute something.
+In this scenario, it is useless to have thousands of threads if we have tens of CPU-cores, virtual threads won't enhance
+the performance of CPU-bound workloads.
+
+
+== Bringing virtual threads to reactive REST services
+Since virtual threads are disposable entities, the fundamental idea of quarkus-loom is to offload the execution of an
+endpoint handler on a new virtual thread instead of running it on an event-loop (in the case of RESTeasy-reactive) or a
+platform worker thread.
+
+To do so, it suffices to add the link:{runonvthread}[@RunOnVirtualThread] annotation to the endpoint.
+If the JDK is compatible (Java 19 or later versions) then the endpoint will be offloaded to a virtual thread.
+It will then be possible to perform blocking operations without blocking the platform thread upon which the virtual
+thread is mounted.
+
+This annotation can only be used in conjunction with endpoints annotated with link:{blockingannotation}[@Blocking] or
+considered blocking because of their signature.
+You can visit xref:resteasy-reactive.adoc#execution-model-blocking-non-blocking[Execution model, blocking, non-blocking]
+for more information.
+
+=== Getting started
+
+Add the following import to your build file:
+
+[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
+.pom.xml
+----
+
+ io.quarkus
+ quarkus-resteasy-reactive
+
+----
+
+[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
+.build.gradle
+----
+implementation("io.quarkus:quarkus-resteasy-reactive")
+----
+
+You also need to make sure that you are using the version 19 of Java, this can be enforced in your pom.xml file with the following:
+
+[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
+.pom.xml
+----
+
+ 19
+ 19
+
+----
+
+Virtual threads are still an experimental feature, you need to start your application with the `--enable-preview` flag:
+
+[source, bash]
+----
+java --enable-preview -jar target/quarkus-app/quarkus-run.jar
+----
+
+The example below shows the differences between three endpoints, all of them querying a fortune in the database then
+returning it to the client.
+
+- the first one uses the traditional blocking style, it is considered blocking due to its signature.
+- the second one uses Mutiny reactive streams in a declarative style, it is considered non-blocking due to its signature.
+- the third one uses Mutiny reactive streams in a synchronous way, since it doesn't return a "reactive type" it is
+considered blocking and the link:{runonvthread}[@RunOnVirtualThread] annotation can be used.
+
+When using Mutiny, alternative "xAndAwait" methods are provided to be used with virtual threads.
+They ensure that waiting for the completion of the I/O will not "pin" the carrier thread and deteriorate performance.
+Pinning is a phenomenon that we describe in xref:Pinning cases[this section].
+
+
+In other words, the mutiny environment is a safe environment for virtual threads.
+The guarantees offered by Mutiny are detailed later.
+
+[source,java]
+----
+package org.acme.rest;
+
+import org.acme.fortune.model.Fortune;
+import org.acme.fortune.repository.FortuneRepository;
+import io.smallrye.common.annotation.RunOnVirtualThread;
+import io.smallrye.mutiny.Uni;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import java.util.List;
+import java.util.Random;
+
+
+@Path("")
+public class FortuneResource {
+
+ @GET
+ @Path("/blocking")
+ public Fortune blocking() {
+ var list = repository.findAllBlocking();
+ return pickOne(list);
+ }
+
+ @GET
+ @Path("/reactive")
+ public Uni reactive() {
+ return repository.findAllAsync()
+ .map(this::pickOne);
+ }
+
+ @GET
+ @Path("/virtual")
+ @RunOnVirtualThread
+ public Fortune virtualThread() {
+ var list = repository.findAllAsyncAndAwait();
+ return pickOne(list);
+ }
+
+}
+----
+
+=== Simplifying complex logic
+The previous example is trivial and doesn't capture how imperative style can simplify complex reactive operations.
+Below is a more complex example.
+The endpoints must now fetch all the fortunes in the database, then append a quote to each fortune before finally returning
+the result to the client.
+
+
+
+[source,java]
+----
+package org.acme.rest;
+
+import org.acme.fortune.model.Fortune;
+import org.acme.fortune.repository.FortuneRepository;
+import io.smallrye.common.annotation.RunOnVirtualThread;
+import io.smallrye.mutiny.Uni;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import java.util.List;
+import java.util.Random;
+
+
+@Path("")
+public class FortuneResource {
+
+ private final FortuneRepository repository;
+
+ public Uni> getQuotesAsync(int size){
+ //...
+ //asynchronously returns a list of quotes from an arbitrary source
+ }
+
+ @GET
+ @Path("/quoted-blocking")
+ public List getAllQuotedBlocking() {
+ // we get the list of fortunes
+ var fortunes = repository.findAllBlocking();
+
+ // we get the list of quotes
+ var quotes = getQuotes(fortunes.size()).await().indefinitely();
+
+ // we append each quote to each fortune
+ for(int i=0; i < fortunes.size();i ++){
+ fortunes.get(i).title+= " - "+quotes.get(i);
+ }
+ return todos;
+ }
+
+ @GET
+ @Path("/quoted-reactive")
+ public Uni> getAllQuoted() {
+ // we first fetch the list of resource and we memoize it
+ // to avoid fetching it again everytime need it
+ var fortunes = repository.findAllAsync().memoize().indefinitely();
+
+ // once we get a result for fortunes,
+ // we know its size and can thus query the right number of quotes
+ var quotes = fortunes.onItem().transformToUni(list -> getQuotes(list.size()));
+
+ // we now need to combine the two reactive streams
+ // before returning the result to the user
+ return Uni.combine().all().unis(fortunes,quotes).asTuple().onItem().transform(tuple -> {
+ var todoList=tuple.getItem1();
+ //can await it since it is already resolved
+ var quotesList = tuple.getItem2();
+ for(int i=0; i < todoList.size();i ++){
+ todoList.get(i).title+= " - "+quotesList.get(i);
+ }
+ return todoList;
+ });
+ }
+
+ @GET
+ @RunOnVirtualThread
+ @Path("/quoted-virtual-thread")
+ public List getAllQuotedBlocking() {
+ //we get the list of fortunes
+ var fortunes = repository.findAllAsyncAndAwait();
+
+ //we get the list of quotes
+ var quotes = getQuotes(fortunes.size()).await().indefinitely();
+
+ //we append each quote to each fortune
+ for(int i=0; i < fortunes.size();i ++){
+ fortunes.get(i).title+= " - "+quotes.get(i);
+ }
+ return todos;
+ }
+
+}
+----
+
+== Pinning cases
+The notion of "cheap blocking" might not always be true: in certain occasions a virtual thread might "pin" its carrier
+(the platform thread it is mounted upon).
+In this situation, the platform thread is blocked exactly as it would have been in a typical blocking scenario.
+
+According to link:{vthreadjep}[JEP 425] this can happen in two situations:
+
+- when a virtual thread executes performs a blocking operation inside a `synchronized` block or method
+- when it executes a blocking operation inside a native method or a foreign function
+
+It can be fairly easy to avoid these situations in our own code, but it is hard to verify every dependency we use.
+Typically, while experimenting with virtual-threads, we realized that using the link:{pgsql-driver}[postgresql-JDBC driver]
+results in frequent pinning.
+
+=== The JDBC problem
+Our experiments so far show that when a virtual thread queries a database using the JDBC driver, it will pin its carrier
+thread during the entire operation.
+
+Let's show the code of the `findAllBlocking()` method we used in the first example
+
+[source, java]
+----
+//import ...
+
+@ApplicationScoped
+public class FortuneRepository {
+ // ...
+
+ public List findAllBlocking() {
+ List fortunes = new ArrayList<>();
+ Connection conn = null;
+ try {
+ conn = db.getJdbcConnection();
+ var preparedStatement = conn.prepareStatement(SELECT_ALL);
+ ResultSet rs = preparedStatement.executeQuery();
+ while (rs.next()) {
+ fortunes.add(create(rs));
+ }
+ rs.close();
+ preparedStatement.close();
+ } catch (SQLException e) {
+ logger.warn("Unable to retrieve fortunes from the database", e);
+ } finally {
+ close(conn);
+ }
+ return fortunes;
+ }
+
+ //...
+}
+----
+
+The actual query happens at `ResultSet rs = preparedStatement.executeQuery();`, here is how it is implemented in the
+postgresql-jdbc driver 42.5.0:
+
+[source, java]
+----
+class PgPreparedStatement extends PgStatement implements PreparedStatement {
+ // ...
+
+ /*
+ * A Prepared SQL query is executed and its ResultSet is returned
+ *
+ * @return a ResultSet that contains the data produced by the * query - never null
+ *
+ * @exception SQLException if a database access error occurs
+ */
+ @Override
+ public ResultSet executeQuery() throws SQLException {
+ synchronized (this) {
+ if (!executeWithFlags(0)) {
+ throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA);
+ }
+ return getSingleResultSet();
+ }
+ }
+
+ // ...
+}
+----
+
+This `synchronized` block is the culprit.
+Replacing it with a lock is a good solution, but it won't be enough: `synchronized` blocks are also used in `executeWithFlags(int flag)`.
+A systematic review of the postgresql-jdbc driver is necessary to make sure that it is compliant with virtual threads.
+
+=== Reactive drivers at the rescue
+The vertx-sql-client is a reactive client, hence it is not supposed to block while waiting for the completion of a
+transaction with the database.
+However, when using the link:{mutiny-vertx-sql}[smallrye-mutiny-vertx-sqlclient] it is possible to use a variant method
+that will await for the completion of the transaction, mimicking a blocking behaviour.
+
+Below is the `FortuneRepository` except the blocking we've seen earlier has been replaced by reactive methods.
+
+[source, java]
+----
+//import ...
+
+@ApplicationScoped
+public class FortuneRepository {
+ // ...
+
+ public Uni> findAllAsync() {
+ return db.getPool()
+ .preparedQuery(SELECT_ALL).execute()
+ .map(this::createListOfFortunes);
+
+ }
+
+ public List findAllAsyncAndAwait() {
+ var rows = db.getPool().preparedQuery(SELECT_ALL)
+ .executeAndAwait();
+ return createListOfFortunes(rows);
+ }
+
+ //...
+}
+----
+
+Contrary to the link:{pgsql-driver}[postgresql-jdbc driver], no `synchronized` block is used where it shouldn't be, and
+the `await` behaviour is implemented using locks and latches that won't cause pinning.
+
+Using the synchronous methods of the link:{mutiny-vertx-sql}[smallrye-mutiny-vertx-sqlclient] along with virtual threads
+will allow you to use the synchronous blocking style, avoid pinning the carrier thread, and get performance close to a pure
+reactive implementation.
+
+== A point about performance
+
+Our experiments seem to indicate that Quarkus with virtual threads will scale better than Quarkus blocking (offloading
+the computation on a pool of platform worker threads) but not as well Quarkus reactive.
+The memory consumption especially might be an issue: if your system needs to keep its memory footprint low we would
+advise you stick to using reactive constructs.
+
+This degradation of performance doesn't seem to come from virtual threads themselves but from the interactions between
+Vert.x/Netty (Quarkus underlying reactive engine) and the virtual threads.
+This was illustrated in the issue that we will now describe.
+
+=== The Netty problem
+For JSON serialization, Netty uses their custom implementation of thread locals, `FastThreadLocal` to store buffers.
+When using virtual threads in quarkus, then number of virtual threads simultaneously living in the service is directly
+related to the incoming traffic.
+It is possible to get hundreds of thousands, if not millions, of them.
+
+If they need to serialize some data to JSON they will end up creating as many instances of `FastThreadLocal`, resulting
+on a massive memory consumption as well as exacerbated pressure on the garbage collector.
+This will eventually affect the performance of the application and inhibit its scalability.
+
+This is a perfect example of the mismatch between the reactive stack and the virtual threads.
+The fundamental hypothesis are completely different and result in different optimizations.
+Netty expects a system using few event-loops (as many event-loops as CPU cores by default in Quarkus), but it gets hundreds
+of thousands of threads.
+You can refer to link:https://mail.openjdk.org/pipermail/loom-dev/2022-July/004844.html[this mail] to get more information
+on how we envision our future with virtual threads.
+
+=== Our solution to the Netty problem
+In order to avoid this wasting of resource without modifying Netty upstream, we wrote an extension that modifies the
+bytecode of the class responsible for creating the thread locals at build time.
+Using this extension, performance of virtual threads in Quarkus for the Json Serialization test of the Techempower suite
+increased by nearly 80%, making it almost as good as reactive endpoints.
+
+To use it, it needs to be added as a dependency:
+
+[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
+.pom.xml
+----
+
+ io.quarkus
+ quarkus-netty-loom-adaptor
+
+----
+
+Furthermore, some operations undertaken by this extension need special access, it is necessary to
+
+- compile the application with the flag `-Dnet.bytebuddy.experimental`
+- open the `java.base.lang` module at runtime with the flag `--add-opens java.base/java.lang=ALL-UNNAMED`
+
+This extension is only intended to improve performance, it is perfectly fine not to use it.
+
+=== Concerning dev mode
+If you want to use quarkus with the dev mode, it won't be possible to manually specify the flags we mentioned along this guide.
+Instead, you want to specify them all in the configuration of the `quarkus-maven-plugin` as presented below.
+
+[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
+.pom.xml
+----
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${quarkus.version}
+
+
+
+ build
+
+
+
+
+
+
+ 19
+
+ --enable-preview
+ -Dnet.bytebuddy.experimental
+
+ --enable-preview --add-opens java.base/java.lang=ALL-UNNAMED
+
+
+
+----
+
+If you don't want to put specify the opening the `java.lang` module in your pom.xml file, you can also specify it as an argument
+when you start the dev mode.
+
+The configuration of the quarkus-maven-plugin will be simpler:
+
+[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
+.pom.xml
+----
+
+
+ 19
+
+ --enable-preview
+ -Dnet.bytebuddy.experimental
+
+ --enable-preview
+
+----
+
+And the command will become:
+
+[source, bash]
+----
+mvn quarkus:dev -Dopen-lang-package
+----
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomFormAuthChallengeTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomFormAuthChallengeTest.java
new file mode 100644
index 0000000000000..696e130ec95d2
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/CustomFormAuthChallengeTest.java
@@ -0,0 +1,108 @@
+package io.quarkus.vertx.http.security;
+
+import java.util.Set;
+import java.util.function.Supplier;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.inject.Alternative;
+import javax.inject.Inject;
+
+import org.hamcrest.Matchers;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.arc.Priority;
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.AuthenticationRequest;
+import io.quarkus.security.test.utils.TestIdentityController;
+import io.quarkus.security.test.utils.TestIdentityProvider;
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+import io.quarkus.vertx.http.runtime.security.FormAuthenticationMechanism;
+import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
+import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
+import io.restassured.RestAssured;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+public class CustomFormAuthChallengeTest {
+
+ private static final int EXPECTED_STATUS = 203;
+ private static final String EXPECTED_HEADER_NAME = "ElizabethII";
+ private static final String EXPECTED_HEADER_VALUE = "CharlesIV";
+ private static final String ADMIN = "admin";
+ private static final String APP_PROPS = "" +
+ "quarkus.http.auth.form.enabled=true\n" +
+ "quarkus.http.auth.form.login-page=login\n" +
+ "quarkus.http.auth.form.error-page=error\n" +
+ "quarkus.http.auth.form.landing-page=landing\n" +
+ "quarkus.http.auth.policy.r1.roles-allowed=admin\n" +
+ "quarkus.http.auth.session.encryption-key=CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT-CHANGEIT\n";
+
+ @RegisterExtension
+ static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() {
+ @Override
+ public JavaArchive get() {
+ return ShrinkWrap.create(JavaArchive.class)
+ .addClasses(CustomFormAuthenticator.class, TestIdentityProvider.class, TestIdentityController.class,
+ TestTrustedIdentityProvider.class, PathHandler.class)
+ .addAsResource(new StringAsset(APP_PROPS), "application.properties");
+ }
+ });
+
+ @BeforeAll
+ public static void setup() {
+ TestIdentityController.resetRoles().add(ADMIN, ADMIN, ADMIN);
+ }
+
+ @Test
+ public void testCustomGetChallengeIsCalled() {
+ RestAssured
+ .given()
+ .when()
+ .formParam("j_username", ADMIN)
+ .formParam("j_password", "wrong_password")
+ .post("/j_security_check")
+ .then()
+ .assertThat()
+ .statusCode(EXPECTED_STATUS)
+ .header(EXPECTED_HEADER_NAME, Matchers.is(EXPECTED_HEADER_VALUE));
+ }
+
+ @Alternative
+ @Priority(1)
+ @ApplicationScoped
+ public static class CustomFormAuthenticator implements HttpAuthenticationMechanism {
+
+ @Inject
+ FormAuthenticationMechanism delegate;
+
+ @Override
+ public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
+ final var authenticate = delegate.authenticate(context, identityProviderManager);
+ context.put(HttpAuthenticationMechanism.class.getName(), this);
+ return authenticate;
+ }
+
+ @Override
+ public Uni getChallenge(RoutingContext context) {
+ return Uni.createFrom().item(new ChallengeData(EXPECTED_STATUS, EXPECTED_HEADER_NAME, EXPECTED_HEADER_VALUE));
+ }
+
+ @Override
+ public Set> getCredentialTypes() {
+ return delegate.getCredentialTypes();
+ }
+
+ @Override
+ public Uni getCredentialTransport(RoutingContext context) {
+ return delegate.getCredentialTransport(context);
+ }
+ }
+
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java
index 0a2ce19a3343d..8b2de1d915bd1 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java
@@ -137,10 +137,14 @@ public Uni sendChallenge(RoutingContext routingContext) {
routingContext.request().resume();
Uni result = null;
- HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName());
- if (matchingMech != null) {
- result = matchingMech.sendChallenge(routingContext);
+ // we only require auth mechanism to put itself into routing context when there is more than one mechanism registered
+ if (mechanisms.length > 1) {
+ HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName());
+ if (matchingMech != null) {
+ result = matchingMech.sendChallenge(routingContext);
+ }
}
+
if (result == null) {
result = mechanisms[0].sendChallenge(routingContext);
for (int i = 1; i < mechanisms.length; ++i) {
@@ -169,9 +173,12 @@ public Uni extends Boolean> apply(Boolean authDone) {
}
public Uni getChallenge(RoutingContext routingContext) {
- HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName());
- if (matchingMech != null) {
- return matchingMech.getChallenge(routingContext);
+ // we only require auth mechanism to put itself into routing context when there is more than one mechanism registered
+ if (mechanisms.length > 1) {
+ HttpAuthenticationMechanism matchingMech = routingContext.get(HttpAuthenticationMechanism.class.getName());
+ if (matchingMech != null) {
+ return matchingMech.getChallenge(routingContext);
+ }
}
Uni result = mechanisms[0].getChallenge(routingContext);
for (int i = 1; i < mechanisms.length; ++i) {