diff --git a/bom/application/pom.xml b/bom/application/pom.xml index fa3095970807a1..8531db6b670148 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1239,6 +1239,16 @@ quarkus-infinispan-client-deployment ${project.version} + + io.quarkus + quarkus-infinispan-client-sessions + ${project.version} + + + io.quarkus + quarkus-infinispan-client-sessions-deployment + ${project.version} + io.quarkus quarkus-jaeger @@ -5972,6 +5982,11 @@ quarkus-redis-client-runtime-spi ${project.version} + + io.quarkus + quarkus-redis-client-sessions + ${project.version} + io.quarkus quarkus-redis-cache @@ -5988,6 +6003,11 @@ quarkus-redis-client-deployment-spi ${project.version} + + io.quarkus + quarkus-redis-client-sessions-deployment + ${project.version} + io.quarkus quarkus-redis-cache-deployment diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index cbdbc050e4dfc4..8dfa44fda4f1cf 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -889,6 +889,19 @@ + + io.quarkus + quarkus-infinispan-client-sessions + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-info @@ -1838,6 +1851,19 @@ + + io.quarkus + quarkus-redis-client-sessions + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-rest-client diff --git a/docs/pom.xml b/docs/pom.xml index dbf502dbfae576..01e3333970ccba 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -905,6 +905,19 @@ + + io.quarkus + quarkus-infinispan-client-sessions-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-info-deployment @@ -1854,6 +1867,19 @@ + + io.quarkus + quarkus-redis-client-sessions-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-rest-client-deployment diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 1cd8362093229f..50e29e51bf5ced 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -610,6 +610,7 @@ Route order constants defined in `io.quarkus.vertx.http.runtime.RouteConstants` | `Integer.MIN_VALUE` | `ROUTE_ORDER_BODY_HANDLER_MANAGEMENT` | Body handler for the management router. | `Integer.MIN_VALUE` | `ROUTE_ORDER_HEADERS` | Handlers that add headers specified in the configuration. | `Integer.MIN_VALUE` | `ROUTE_ORDER_CORS_MANAGEMENT` | CORS-Origin handler of the management router. +| `Integer.MIN_VALUE` | `ROUTE_ORDER_SESSION_HANDLER` | Session handler, if enabled in the configuration. | `Integer.MIN_VALUE + 1` | `ROUTE_ORDER_BODY_HANDLER` | Body handler. | `-2` | `ROUTE_ORDER_UPLOAD_LIMIT` | Route that enforces the upload body size limit. | `0` | `ROUTE_ORDER_COMPRESSION` | Compression handler. diff --git a/extensions/infinispan-client/pom.xml b/extensions/infinispan-client/pom.xml index 4bcaaeebab1194..771ef999e2409b 100644 --- a/extensions/infinispan-client/pom.xml +++ b/extensions/infinispan-client/pom.xml @@ -18,5 +18,8 @@ deployment-spi runtime runtime-spi + + sessions/deployment + sessions/runtime diff --git a/extensions/infinispan-client/runtime/pom.xml b/extensions/infinispan-client/runtime/pom.xml index 91440a2169ae3c..adc82bcf5e15b8 100644 --- a/extensions/infinispan-client/runtime/pom.xml +++ b/extensions/infinispan-client/runtime/pom.xml @@ -147,6 +147,11 @@ quarkus-kubernetes-service-binding true + + io.quarkus + quarkus-infinispan-client-sessions + true + io.quarkus quarkus-junit5-internal diff --git a/extensions/infinispan-client/sessions/deployment/pom.xml b/extensions/infinispan-client/sessions/deployment/pom.xml new file mode 100644 index 00000000000000..ee7cdc70de254a --- /dev/null +++ b/extensions/infinispan-client/sessions/deployment/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.quarkus + quarkus-infinispan-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-infinispan-client-sessions-deployment + + Quarkus - Infinispan Client - Vert.x Web Sessions - Deployment + + + io.quarkus + quarkus-infinispan-client-sessions + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-vertx-http-deployment + + + io.quarkus + quarkus-infinispan-client-deployment-spi + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsBuildTimeConfig.java b/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsBuildTimeConfig.java new file mode 100644 index 00000000000000..20a830a26f1d60 --- /dev/null +++ b/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsBuildTimeConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.infinispan.sessions.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Configuration of Vert.x Web sessions stored in remote Infinispan cache. + */ +@ConfigRoot(name = "http.sessions.infinispan", phase = ConfigPhase.BUILD_TIME) +public class InfinispanSessionsBuildTimeConfig { + /** + * Name of the Infinispan client configured in the Quarkus Infinispan Client extension configuration. + * If not set, uses the default (unnamed) Infinispan client. + *

+ * Note that the Infinispan client must be configured to so that the user has necessary permissions + * on the Infinispan server. The required minimum is the Infinispan {@code deployer} role. + */ + @ConfigItem + public Optional clientName; +} diff --git a/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsProcessor.java b/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsProcessor.java new file mode 100644 index 00000000000000..111c4c8b5c0b50 --- /dev/null +++ b/extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsProcessor.java @@ -0,0 +1,46 @@ +package io.quarkus.infinispan.sessions.deployment; + +import java.util.List; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.infinispan.client.deployment.spi.InfinispanClientBuildItem; +import io.quarkus.infinispan.client.deployment.spi.InfinispanClientNameBuildItem; +import io.quarkus.infinispan.client.runtime.spi.InfinispanConstants; +import io.quarkus.infinispan.sessions.runtime.InfinispanSessionsRecorder; +import io.quarkus.vertx.http.deployment.SessionStoreProviderBuildItem; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.SessionsBuildTimeConfig; + +public class InfinispanSessionsProcessor { + @BuildStep + public void infinispanClients(HttpBuildTimeConfig httpConfig, + InfinispanSessionsBuildTimeConfig config, + BuildProducer infinispanRequest) { + if (httpConfig.sessions.mode == SessionsBuildTimeConfig.SessionsMode.INFINISPAN) { + String clientName = config.clientName.orElse(InfinispanConstants.DEFAULT_INFINISPAN_CLIENT_NAME); + infinispanRequest.produce(new InfinispanClientNameBuildItem(clientName)); + } + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void infinispanSessions(HttpBuildTimeConfig httpConfig, + InfinispanSessionsBuildTimeConfig config, + List clients, + BuildProducer provider, + InfinispanSessionsRecorder recorder) { + if (httpConfig.sessions.mode == SessionsBuildTimeConfig.SessionsMode.INFINISPAN) { + String clientName = config.clientName.orElse(InfinispanConstants.DEFAULT_INFINISPAN_CLIENT_NAME); + for (InfinispanClientBuildItem infinispanClient : clients) { + if (clientName.equals(infinispanClient.getName())) { + provider.produce(new SessionStoreProviderBuildItem(recorder.create(infinispanClient.getClient()))); + return; + } + } + throw new IllegalStateException("Unknown Infinispan client: " + clientName); + } + } +} diff --git a/extensions/infinispan-client/sessions/runtime/pom.xml b/extensions/infinispan-client/sessions/runtime/pom.xml new file mode 100644 index 00000000000000..fd9d9dd2dad3bf --- /dev/null +++ b/extensions/infinispan-client/sessions/runtime/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + + io.quarkus + quarkus-infinispan-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-infinispan-client-sessions + + Quarkus - Infinispan Client - Vert.x Web Sessions - Runtime + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-vertx-http + + + io.vertx + vertx-web-sstore-infinispan + + + org.infinispan + infinispan-client-hotrod + + + io.reactivex.rxjava3 + rxjava + + + + + org.infinispan + infinispan-client-hotrod-jakarta + + + org.infinispan + infinispan-jboss-marshalling + + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.2_spec + + + io.netty + netty-transport-native-epoll + + + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + io.quarkus:quarkus-vertx-http + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsConfig.java b/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsConfig.java new file mode 100644 index 00000000000000..56c332f2d1b362 --- /dev/null +++ b/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsConfig.java @@ -0,0 +1,28 @@ +package io.quarkus.infinispan.sessions.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Configuration of Vert.x Web sessions stored in remote Infinispan cache. + */ +@ConfigRoot(name = "http.sessions.infinispan", phase = ConfigPhase.RUN_TIME) +public class InfinispanSessionsConfig { + /** + * Name of the Infinispan cache used to store session data. If it does not exist, it is created + * automatically from Infinispan's default template {@code DIST_SYNC}. + */ + @ConfigItem(defaultValue = "quarkus.sessions") + public String cacheName; + + /** + * Maximum time to retry when retrieving session data from the Infinispan cache. + * The Vert.x session handler retries when the session data are not found, because + * distributing data across an Infinispan cluster may take time. + */ + @ConfigItem(defaultValue = "5s") + public Duration retryTimeout; +} diff --git a/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsRecorder.java b/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsRecorder.java new file mode 100644 index 00000000000000..c82b315dfb0a79 --- /dev/null +++ b/extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsRecorder.java @@ -0,0 +1,38 @@ +package io.quarkus.infinispan.sessions.runtime; + +import java.time.Duration; +import java.util.function.Supplier; + +import org.infinispan.client.hotrod.RemoteCacheManager; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.core.runtime.VertxCoreRecorder; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.sstore.SessionStore; +import io.vertx.ext.web.sstore.infinispan.InfinispanSessionStore; + +@Recorder +public class InfinispanSessionsRecorder { + private final RuntimeValue config; + + public InfinispanSessionsRecorder(RuntimeValue config) { + this.config = config; + } + + public Supplier create(RuntimeValue client) { + return new Supplier() { + @Override + public SessionStore get() { + Vertx vertx = VertxCoreRecorder.getVertx().get(); + String cacheName = config.getValue().cacheName; + Duration retryTimeout = config.getValue().retryTimeout; + JsonObject options = new JsonObject() + .put("cacheName", cacheName) + .put("retryTimeout", retryTimeout.toMillis()); + return InfinispanSessionStore.create(vertx, options, client.getValue()); + } + }; + } +} diff --git a/extensions/infinispan-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/infinispan-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..476e398efc5ae4 --- /dev/null +++ b/extensions/infinispan-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,13 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Infinispan Client - Vert.x Web Sessions" +metadata: + keywords: + - "infinispan" + - "vertx" + - "sessions" + guide: "https://quarkus.io/guides/http-reference#vertx-web-sessions" + categories: + - "web" + status: "preview" + unlisted: true diff --git a/extensions/redis-client/pom.xml b/extensions/redis-client/pom.xml index c861fb139cc9f7..543702fa281ba7 100644 --- a/extensions/redis-client/pom.xml +++ b/extensions/redis-client/pom.xml @@ -21,6 +21,9 @@ deployment-spi runtime runtime-spi + + sessions/deployment + sessions/runtime diff --git a/extensions/redis-client/runtime/pom.xml b/extensions/redis-client/runtime/pom.xml index 3dd920a374e64b..2ae90adb17c231 100644 --- a/extensions/redis-client/runtime/pom.xml +++ b/extensions/redis-client/runtime/pom.xml @@ -37,6 +37,11 @@ quarkus-smallrye-health true + + io.quarkus + quarkus-redis-client-sessions + true + org.assertj assertj-core diff --git a/extensions/redis-client/sessions/deployment/pom.xml b/extensions/redis-client/sessions/deployment/pom.xml new file mode 100644 index 00000000000000..16b78ba733b580 --- /dev/null +++ b/extensions/redis-client/sessions/deployment/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.quarkus + quarkus-redis-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-redis-client-sessions-deployment + + Quarkus - Redis Client - Vert.x Web Sessions - Deployment + + + io.quarkus + quarkus-redis-client-sessions + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-vertx-http-deployment + + + io.quarkus + quarkus-redis-client-deployment-spi + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsBuildTimeConfig.java b/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsBuildTimeConfig.java new file mode 100644 index 00000000000000..caf5991dcba042 --- /dev/null +++ b/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsBuildTimeConfig.java @@ -0,0 +1,20 @@ +package io.quarkus.redis.sessions.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Configuration of Vert.x Web sessions stored in Redis. + */ +@ConfigRoot(name = "http.sessions.redis", phase = ConfigPhase.BUILD_TIME) +public class RedisSessionsBuildTimeConfig { + /** + * Name of the Redis client configured in the Quarkus Redis extension configuration. + * If not set, uses the default (unnamed) Redis client. + */ + @ConfigItem + public Optional clientName; +} diff --git a/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsProcessor.java b/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsProcessor.java new file mode 100644 index 00000000000000..2d6f3091ac1205 --- /dev/null +++ b/extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsProcessor.java @@ -0,0 +1,46 @@ +package io.quarkus.redis.sessions.deployment; + +import java.util.List; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.redis.deployment.client.spi.RedisClientBuildItem; +import io.quarkus.redis.deployment.client.spi.RequestedRedisClientBuildItem; +import io.quarkus.redis.runtime.spi.RedisConstants; +import io.quarkus.redis.sessions.runtime.RedisSessionsRecorder; +import io.quarkus.vertx.http.deployment.SessionStoreProviderBuildItem; +import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.SessionsBuildTimeConfig; + +public class RedisSessionsProcessor { + @BuildStep + public void redisClients(HttpBuildTimeConfig httpConfig, + RedisSessionsBuildTimeConfig config, + BuildProducer redisRequest) { + if (httpConfig.sessions.mode == SessionsBuildTimeConfig.SessionsMode.REDIS) { + String clientName = config.clientName.orElse(RedisConstants.DEFAULT_CLIENT_NAME); + redisRequest.produce(new RequestedRedisClientBuildItem(clientName)); + } + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public void redisSessions(HttpBuildTimeConfig httpConfig, + RedisSessionsBuildTimeConfig config, + List clients, + BuildProducer provider, + RedisSessionsRecorder recorder) { + if (httpConfig.sessions.mode == SessionsBuildTimeConfig.SessionsMode.REDIS) { + String clientName = config.clientName.orElse(RedisConstants.DEFAULT_CLIENT_NAME); + for (RedisClientBuildItem redisClient : clients) { + if (clientName.equals(redisClient.getName())) { + provider.produce(new SessionStoreProviderBuildItem(recorder.create(redisClient.getClient()))); + return; + } + } + throw new IllegalStateException("Unknown Redis client: " + clientName); + } + } +} diff --git a/extensions/redis-client/sessions/runtime/pom.xml b/extensions/redis-client/sessions/runtime/pom.xml new file mode 100644 index 00000000000000..6490629ee0e028 --- /dev/null +++ b/extensions/redis-client/sessions/runtime/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + io.quarkus + quarkus-redis-client-parent + 999-SNAPSHOT + ../../pom.xml + + + quarkus-redis-client-sessions + + Quarkus - Redis Client - Vert.x Web Sessions - Runtime + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-vertx-http + + + io.vertx + vertx-web-sstore-redis + + + io.smallrye.reactive + smallrye-mutiny-vertx-redis-client + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + io.quarkus:quarkus-vertx-http + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsConfig.java b/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsConfig.java new file mode 100644 index 00000000000000..defd5be8cded97 --- /dev/null +++ b/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsConfig.java @@ -0,0 +1,21 @@ +package io.quarkus.redis.sessions.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Configuration of Vert.x Web sessions stored in Redis. + */ +@ConfigRoot(name = "http.sessions.redis", phase = ConfigPhase.RUN_TIME) +public class RedisSessionsConfig { + /** + * Maximum time to retry when retrieving session data from the Redis server. + * The Vert.x session handler retries when the session data are not found, because + * distributing data across a potential Redis cluster may take some time. + */ + @ConfigItem(defaultValue = "2s") + public Duration retryTimeout; +} diff --git a/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsRecorder.java b/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsRecorder.java new file mode 100644 index 00000000000000..3608e74acc464b --- /dev/null +++ b/extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsRecorder.java @@ -0,0 +1,32 @@ +package io.quarkus.redis.sessions.runtime; + +import java.time.Duration; +import java.util.function.Supplier; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.core.runtime.VertxCoreRecorder; +import io.vertx.core.Vertx; +import io.vertx.ext.web.sstore.SessionStore; +import io.vertx.ext.web.sstore.redis.RedisSessionStore; +import io.vertx.mutiny.redis.client.Redis; + +@Recorder +public class RedisSessionsRecorder { + private final RuntimeValue config; + + public RedisSessionsRecorder(RuntimeValue config) { + this.config = config; + } + + public Supplier create(Supplier client) { + return new Supplier() { + @Override + public SessionStore get() { + Vertx vertx = VertxCoreRecorder.getVertx().get(); + Duration retryTimeout = config.getValue().retryTimeout; + return RedisSessionStore.create(vertx, retryTimeout.toMillis(), client.get().getDelegate()); + } + }; + } +} diff --git a/extensions/redis-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/redis-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..4f4ef6cbe8e9b4 --- /dev/null +++ b/extensions/redis-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,13 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Redis Client - Vert.x Web Sessions" +metadata: + keywords: + - "redis" + - "vertx" + - "sessions" + guide: "https://quarkus.io/guides/http-reference#vertx-web-sessions" + categories: + - "web" + status: "preview" + unlisted: true diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index ed3dd67a09031b..8d3eff0acb0e04 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -33,7 +33,7 @@ io.quarkus quarkus-kubernetes-spi - + io.quarkus diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/SessionStoreProviderBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/SessionStoreProviderBuildItem.java new file mode 100644 index 00000000000000..d3a9a87fc1389b --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/SessionStoreProviderBuildItem.java @@ -0,0 +1,24 @@ +package io.quarkus.vertx.http.deployment; + +import java.util.Objects; +import java.util.function.Supplier; + +import io.quarkus.builder.item.MultiBuildItem; +import io.vertx.ext.web.sstore.SessionStore; + +/** + * This is a {@code MultiBuildItem} so that multiple producers may exist + * among the set of currently present extensions. However, at most one item + * of this type may be produced. + */ +public final class SessionStoreProviderBuildItem extends MultiBuildItem { + private final Supplier provider; + + public SessionStoreProviderBuildItem(Supplier provider) { + this.provider = Objects.requireNonNull(provider); + } + + public Supplier getProvider() { + return provider; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index fd8ec2b3ce36e4..233f14893dcd19 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Collectors; @@ -60,6 +61,7 @@ import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; +import io.quarkus.vertx.http.runtime.SessionsBuildTimeConfig; import io.quarkus.vertx.http.runtime.VertxConfigBuilder; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; import io.quarkus.vertx.http.runtime.attribute.ExchangeAttributeBuilder; @@ -72,6 +74,7 @@ import io.vertx.core.impl.VertxImpl; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.sstore.SessionStore; class VertxHttpProcessor { @@ -268,7 +271,7 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, } } - /** + /* * To create mainrouter when `${quarkus.http.root-path}` is not {@literal /} * Refer https://github.com/quarkusio/quarkus/issues/34261 */ @@ -314,7 +317,9 @@ ServiceStartBuildItem finalizeRouter( ShutdownConfig shutdownConfig, LiveReloadConfig lrc, CoreVertxBuildItem core, // Injected to be sure that Vert.x has been produced before calling this method. - ExecutorBuildItem executorBuildItem) + ExecutorBuildItem executorBuildItem, + List sessionStoreProvider, + Capabilities capabilities) throws BuildException, IOException { Optional defaultRoute; @@ -366,6 +371,41 @@ ServiceStartBuildItem finalizeRouter( } } + if (httpBuildTimeConfig.sessions.mode != SessionsBuildTimeConfig.SessionsMode.DISABLED + && capabilities.isPresent(Capability.SERVLET)) { + throw new IllegalStateException("Vert.x Web sessions may not be enabled together with Undertow; " + + "use Undertow (servlet) sessions instead"); + } + + Supplier sessionStore = null; + switch (httpBuildTimeConfig.sessions.mode) { + case DISABLED: + break; + case IN_MEMORY: + sessionStore = recorder.createInMemorySessionStore(); + break; + case REDIS: + if (sessionStoreProvider.isEmpty()) { + throw new IllegalStateException("Redis-based session store was configured, " + + "but the Quarkus Redis Client extension is missing"); + } + if (sessionStoreProvider.size() > 1) { + throw new IllegalStateException("Internal error, multiple session store providers exist"); + } + sessionStore = sessionStoreProvider.get(0).getProvider(); + break; + case INFINISPAN: + if (sessionStoreProvider.isEmpty()) { + throw new IllegalStateException("Infinispan-based session store was configured, " + + "but the Quarkus Infinispan Client extension is missing"); + } + if (sessionStoreProvider.size() > 1) { + throw new IllegalStateException("Internal error, multiple session store providers exist"); + } + sessionStore = sessionStoreProvider.get(0).getProvider(); + break; + } + recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, listOfManagementInterfaceFilters, @@ -376,7 +416,8 @@ ServiceStartBuildItem finalizeRouter( nonApplicationRootPathBuildItem.getNonApplicationRootPath(), launchMode.getLaunchMode(), !requireBodyHandlerBuildItems.isEmpty(), bodyHandler, gracefulShutdownFilter, - shutdownConfig, executorBuildItem.getExecutorProxy()); + shutdownConfig, executorBuildItem.getExecutorProxy(), + sessionStore); return new ServiceStartBuildItem("vertx-http"); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java index 217c01185a875a..85d077e91ae4fa 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CurrentVertxRequest.java @@ -4,6 +4,7 @@ import jakarta.enterprise.inject.Produces; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.Session; @RequestScoped public class CurrentVertxRequest { @@ -17,6 +18,16 @@ public RoutingContext getCurrent() { return current; } + @Produces + @RequestScoped + public Session getCurrentSession() { + Session result = current.session(); + if (result == null) { + throw new UnsupportedOperationException("No active session or support for sessions disabled"); + } + return result; + } + public CurrentVertxRequest setCurrent(RoutingContext current) { this.current = current; return this; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java index c1a2819bd3a88e..99d2f1e6a8ebc6 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java @@ -98,4 +98,10 @@ public class HttpBuildTimeConfig { */ @ConfigItem public OptionalInt compressionLevel; + + /** + * Configuration of Vert.x Web sessions. + */ + @ConfigItem + public SessionsBuildTimeConfig sessions; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 89ffdf53d0c198..749c01bad42c69 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -265,6 +265,11 @@ public class HttpConfiguration { @ConfigItem public Map filter; + /** + * Configuration of Vert.x Web sessions. + */ + public SessionsConfig sessions; + public ProxyConfig proxy; public int determinePort(LaunchMode launchMode) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java index 6d00a3afa9b078..dd8c286dc94f7b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/RouteConstants.java @@ -1,7 +1,7 @@ package io.quarkus.vertx.http.runtime; /** - * Route order value constants used in Quarkus, update {@code reactive-routes.adoc} when changing this class. + * Route order value constants used in Quarkus, update {@code http-reference.adoc} when changing this class. */ @SuppressWarnings("JavadocDeclaration") public final class RouteConstants { @@ -33,6 +33,10 @@ private RouteConstants() { * Order value ({@value #ROUTE_ORDER_CORS_MANAGEMENT}) for the CORS-Origin handler of the management router. */ public static final int ROUTE_ORDER_CORS_MANAGEMENT = Integer.MIN_VALUE; + /** + * Order value ({@value #ROUTE_ORDER_SESSION_HANDLER}) for the session handler, if enabled in the configuration. + */ + public static final int ROUTE_ORDER_SESSION_HANDLER = Integer.MIN_VALUE; /** * Order value ({@value #ROUTE_ORDER_BODY_HANDLER}) for the body handler. */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java new file mode 100644 index 00000000000000..ae1e6dc3e9da60 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java @@ -0,0 +1,48 @@ +package io.quarkus.vertx.http.runtime; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions. + */ +@ConfigGroup +public class SessionsBuildTimeConfig { + /** + * Whether Vert.x Web support for sessions is enabled (the {@code SessionHandler} is added to the router) + * and if so, which session store is used. For the {@code redis} and {@code infinispan} modes, the corresponding + * Quarkus extension must be present and a connection to the data store must be configured there. + */ + @ConfigItem(defaultValue = "disabled") + public SessionsMode mode; + + public enum SessionsMode { + /** + * Support for Vert.x Web sessions is disabled. + */ + DISABLED, + /** + * Support for Vert.x Web sessions is enabled and sessions are stored in memory. + *

+ * In this mode, if an application is deployed in multiple replicas fronted with a load balancer, + * it is necessary to enable sticky sessions (also known as session affinity) on the load balancer. + * Still, losing a replica means losing all sessions stored on that replica. + *

+ * In a multi-replica deployment, it is recommended to use an external session store (Redis or Infinispan). + * Alternatively, if Vert.x clustering is enabled ({@code quarkus.vertx.cluster}), in-memory sessions + * may be clustered ({@code quarkus.http.sessions.in-memory.clustered}), which also makes sticky sessions + * not necessary and prevents session data loss (depending on the Vert.x cluster manager configuration). + */ + IN_MEMORY, + /** + * Support for Vert.x Web sessions is enabled and sessions are stored in a remote Redis server. + * The Quarkus Redis Client extension must be present and a Redis connection must be configured. + */ + REDIS, + /** + * Support for Vert.x Web sessions is enabled and sessions are stored in a remote Infinispan cache. + * The Quarkus Infinispan Client extension must be present and an Infinispan connection must be configured. + */ + INFINISPAN, + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java new file mode 100644 index 00000000000000..7e6dda44fe7743 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java @@ -0,0 +1,102 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.vertx.core.http.CookieSameSite; + +/** + * Configuration of Vert.x Web sessions. + */ +@ConfigGroup +public class SessionsConfig { + /** + * The session timeout. + */ + @ConfigItem(defaultValue = "30M") + public Duration timeout; + + /** + * The requested length of the session identifier. + */ + @ConfigItem(defaultValue = "16") + public int idLength; + + /** + * The session cookie path. The value is relative to {@code quarkus.http.root-path}. + */ + @ConfigItem(defaultValue = "/") + public String path; + + /** + * The name of the session cookie. + */ + @ConfigItem(defaultValue = "JSESSIONID") + public String cookieName; + + /** + * Whether the session cookie has the {@code HttpOnly} attribute. + */ + @ConfigItem(defaultValue = "true") + public boolean cookieHttpOnly; + + /** + * Whether the session cookie has the {@code Secure} attribute. + *

+ */ + @ConfigItem(defaultValue = "auto") + public SessionCookieSecure cookieSecure; + + /** + * The {@code SameSite} attribute of the session cookie. + */ + @ConfigItem + public Optional cookieSameSite; // TODO maybe not `Optional` and default to `strict`? + + /** + * The {@code Max-Age} attribute of the session cookie. Note that setting this option turns the session cookie + * into a persistent cookie. + */ + @ConfigItem + public Optional cookieMaxAge; + + /** + * Configuration of sessions stored in memory. + */ + public SessionsInMemoryConfig inMemory; + + public enum SessionCookieSecure { + /** + * The session cookie only has the {@code Secure} attribute when {@code quarkus.http.insecure-requests} + * is {@code redirect} or {@code disabled}. If {@code insecure-requests} is {@code enabled}, the session cookie + * does not have the {@code Secure} attribute. + */ + AUTO, + /** + * The session cookie always has the {@code Secure} attribute. + */ + ALWAYS, + /** + * The session cookie never has the {@code Secure} attribute. + */ + NEVER; + + boolean isEnabled(HttpConfiguration.InsecureRequests insecureRequests) { + if (this == ALWAYS) { + return true; + } else if (this == NEVER) { + return false; + } else { + return insecureRequests != HttpConfiguration.InsecureRequests.ENABLED; + } + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java new file mode 100644 index 00000000000000..abaee2dada4a4f --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java @@ -0,0 +1,36 @@ +package io.quarkus.vertx.http.runtime; + +import java.time.Duration; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration of Vert.x Web sessions stored in memory. + */ +@ConfigGroup +public class SessionsInMemoryConfig { + /** + * Name of the Vert.x local map or cluster-wide map to store the session data. + */ + @ConfigItem(defaultValue = "quarkus.sessions") + public String mapName; + + /** + * Whether in-memory sessions are clustered. + *

+ * Ignored when Vert.x clustering is not enabled. + */ + @ConfigItem(defaultValue = "false") + public boolean clustered; + + /** + * Maximum time to retry when retrieving session data from the cluster-wide map. + * The Vert.x session handler retries when the session data are not found, because + * distributing data across the cluster may take time. + *

+ * Ignored when in-memory sessions are not clustered. + */ + @ConfigItem(defaultValue = "5s") + public Duration retryTimeout; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index fbcb893f48b9dc..054edbe294ba6c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -7,6 +7,7 @@ import java.net.BindException; import java.net.URI; import java.net.URISyntaxException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -104,6 +105,10 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.CorsHandler; +import io.vertx.ext.web.handler.SessionHandler; +import io.vertx.ext.web.sstore.ClusteredSessionStore; +import io.vertx.ext.web.sstore.LocalSessionStore; +import io.vertx.ext.web.sstore.SessionStore; @Recorder public class VertxHttpRecorder { @@ -191,14 +196,18 @@ private boolean uriValid(HttpServerRequest httpServerRequest) { final RuntimeValue managementConfiguration; private static volatile Handler managementRouter; + final RuntimeValue vertxConfiguration; + public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig, ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, RuntimeValue httpConfiguration, - RuntimeValue managementConfiguration) { + RuntimeValue managementConfiguration, + RuntimeValue vertxConfiguration) { this.httpBuildTimeConfig = httpBuildTimeConfig; this.httpConfiguration = httpConfiguration; this.managementBuildTimeConfig = managementBuildTimeConfig; this.managementConfiguration = managementConfiguration; + this.vertxConfiguration = vertxConfiguration; } public static void setHotReplacement(Handler handler, HotReplacementContext hrc) { @@ -346,6 +355,25 @@ public void mountFrameworkRouter(RuntimeValue mainRouter, RuntimeValue createInMemorySessionStore() { + return new Supplier() { + @Override + public SessionStore get() { + Vertx vertx = VertxCoreRecorder.getVertx().get(); + if (httpConfiguration.getValue().sessions.inMemory.clustered + && vertxConfiguration.getValue().cluster() != null + && vertxConfiguration.getValue().cluster().clustered()) { + return ClusteredSessionStore.create(vertx, + httpConfiguration.getValue().sessions.inMemory.mapName, + httpConfiguration.getValue().sessions.inMemory.retryTimeout.toMillis()); + } else { + // TODO maybe make reaper interval also configurable? + return LocalSessionStore.create(vertx, httpConfiguration.getValue().sessions.inMemory.mapName); + } + } + }; + } + public void finalizeRouter(BeanContainer container, Consumer defaultRouteHandler, List filterList, List managementInterfaceFilterList, Supplier vertx, LiveReloadConfig liveReloadConfig, Optional> mainRouterRuntimeValue, @@ -355,7 +383,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute LaunchMode launchMode, boolean requireBodyHandler, Handler bodyHandler, GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig, - Executor executor) { + Executor executor, Supplier sessionStore) { HttpConfiguration httpConfiguration = this.httpConfiguration.getValue(); // install the default route at the end Router httpRouteRouter = httpRouterRuntimeValue.getValue(); @@ -413,6 +441,23 @@ public void handle(RoutingContext routingContext) { // Headers sent on any request, regardless of the response HttpServerCommonHandlers.applyHeaders(httpConfiguration.header, httpRouteRouter); + if (sessionStore != null) { + SessionsConfig sessions = httpConfiguration.sessions; + String path = httpBuildTimeConfig.rootPath + + (httpBuildTimeConfig.rootPath.endsWith("/") ? "" : "/") + + (sessions.path.startsWith("/") ? sessions.path.substring(1) : sessions.path); + SessionHandler sessionHandler = SessionHandler.create(sessionStore.get()) + .setSessionTimeout(sessions.timeout.toMillis()) + .setMinLength(sessions.idLength) + .setSessionCookiePath(path) + .setSessionCookieName(sessions.cookieName) + .setCookieHttpOnlyFlag(sessions.cookieHttpOnly) + .setCookieSecureFlag(sessions.cookieSecure.isEnabled(httpConfiguration.insecureRequests)) + .setCookieSameSite(sessions.cookieSameSite.orElse(null)) + .setCookieMaxAge(sessions.cookieMaxAge.map(Duration::toMillis).orElse(-1L)); + httpRouteRouter.route().order(RouteConstants.ROUTE_ORDER_ACCESS_LOG_HANDLER).handler(sessionHandler); + } + Handler root; if (rootPath.equals("/")) { if (hotReplacementHandler != null) { diff --git a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/websessions/CounterResource.java b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/websessions/CounterResource.java new file mode 100644 index 00000000000000..d5f008f62cb1b9 --- /dev/null +++ b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/websessions/CounterResource.java @@ -0,0 +1,21 @@ +package io.quarkus.it.infinispan.client.websessions; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.vertx.ext.web.Session; + +@Path("/counter") +public class CounterResource { + @Inject + Session session; + + @GET + public String counter() { + Integer counter = session.get("counter"); + counter = counter == null ? 1 : counter + 1; + session.put("counter", counter); + return session.id() + "|" + counter; + } +} diff --git a/integration-tests/infinispan-client/src/main/resources/application.properties b/integration-tests/infinispan-client/src/main/resources/application.properties index d9e2391da36e85..d1dfb6129874e9 100644 --- a/integration-tests/infinispan-client/src/main/resources/application.properties +++ b/integration-tests/infinispan-client/src/main/resources/application.properties @@ -21,3 +21,4 @@ quarkus.infinispan-client.another.devservices.mcast-port=46667 quarkus.infinispan-client.another.devservices.port=31223 quarkus.infinispan-client.another.devservices.service-name=infinispanAnother +quarkus.http.sessions.mode=infinispan diff --git a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/websessions/CounterTest.java b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/websessions/CounterTest.java new file mode 100644 index 00000000000000..28cfe99241e1f9 --- /dev/null +++ b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/websessions/CounterTest.java @@ -0,0 +1,93 @@ +package io.quarkus.it.infinispan.client.websessions; + +import static io.restassured.RestAssured.with; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.filter.session.SessionFilter; +import io.restassured.response.Response; + +@QuarkusTest +public class CounterTest { + @Test + public void test() throws InterruptedException { + List users = new ArrayList<>(); + for (int i = 0; i < 50; i++) { + users.add(new User(20)); + } + + for (User user : users) { + user.start(); + } + for (User user : users) { + user.join(); + } + for (User user : users) { + user.verify(); + } + } + + static class User extends Thread { + private static final AtomicInteger counter = new AtomicInteger(); + + private final Set sessionIds = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Queue responses = new ConcurrentLinkedQueue<>(); + + private final int requests; + + User(int requests) { + super("User" + counter.incrementAndGet()); + this.requests = requests; + } + + @Override + public void run() { + SessionFilter sessions = new SessionFilter(); + for (int i = 0; i < requests; i++) { + Response response = with().filter(sessions).get("/counter"); + if (response.sessionId() != null) { + sessionIds.add(response.sessionId()); + } + responses.add(response.body().asString()); + + try { + // need to sleep longer to give the session store some time to finish + // + // the operation to store session data into Infinispan is fired off when response headers are written, + // but there's nothing waiting for that operation to complete when the response is being sent + // + // therefore, if we send a 2nd request too quickly after receiving the 1st response, + // the session data may still be in the process of being stored and the 2nd request + // would get stale session data + Thread.sleep(500 + ThreadLocalRandom.current().nextInt(500)); + } catch (InterruptedException e) { + return; + } + } + } + + public void verify() { + assertEquals(1, sessionIds.size()); + String id = sessionIds.iterator().next(); + + assertEquals(requests, responses.size()); + int i = 1; + for (String response : responses) { + assertEquals(id + "|" + i, response); + i++; + } + } + } +} diff --git a/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/websessions/CounterResource.java b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/websessions/CounterResource.java new file mode 100644 index 00000000000000..ce4e905d1bb004 --- /dev/null +++ b/integration-tests/redis-client/src/main/java/io/quarkus/redis/it/websessions/CounterResource.java @@ -0,0 +1,21 @@ +package io.quarkus.redis.it.websessions; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.vertx.ext.web.Session; + +@Path("/counter") +public class CounterResource { + @Inject + Session session; + + @GET + public String counter() { + Integer counter = session.get("counter"); + counter = counter == null ? 1 : counter + 1; + session.put("counter", counter); + return session.id() + "|" + counter; + } +} diff --git a/integration-tests/redis-client/src/main/resources/application.properties b/integration-tests/redis-client/src/main/resources/application.properties index 6bb245e908d5ed..caaa9935687701 100644 --- a/integration-tests/redis-client/src/main/resources/application.properties +++ b/integration-tests/redis-client/src/main/resources/application.properties @@ -8,4 +8,10 @@ quarkus.redis.instance-client.hosts=redis://localhost:6379/5 # use DB 3 quarkus.redis.provided-hosts.hosts-provider-name=test-hosts-provider -quarkus.redis.load-script=starwars.redis \ No newline at end of file +quarkus.redis.load-script=starwars.redis + +quarkus.redis.web-sessions.hosts=redis://localhost:6379/7 +quarkus.redis.web-sessions.max-pool-waiting=100 + +quarkus.http.sessions.mode=redis +quarkus.http.sessions.redis.client-name=web-sessions diff --git a/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/websessions/CounterTest.java b/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/websessions/CounterTest.java new file mode 100644 index 00000000000000..0de4a5b639809e --- /dev/null +++ b/integration-tests/redis-client/src/test/java/io/quarkus/redis/it/websessions/CounterTest.java @@ -0,0 +1,93 @@ +package io.quarkus.redis.it.websessions; + +import static io.restassured.RestAssured.with; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.filter.session.SessionFilter; +import io.restassured.response.Response; + +@QuarkusTest +public class CounterTest { + @Test + public void test() throws InterruptedException { + List users = new ArrayList<>(); + for (int i = 0; i < 50; i++) { + users.add(new User(20)); + } + + for (User user : users) { + user.start(); + } + for (User user : users) { + user.join(); + } + for (User user : users) { + user.verify(); + } + } + + static class User extends Thread { + private static final AtomicInteger counter = new AtomicInteger(); + + private final Set sessionIds = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Queue responses = new ConcurrentLinkedQueue<>(); + + private final int requests; + + User(int requests) { + super("User" + counter.incrementAndGet()); + this.requests = requests; + } + + @Override + public void run() { + SessionFilter sessions = new SessionFilter(); + for (int i = 0; i < requests; i++) { + Response response = with().filter(sessions).get("/counter"); + if (response.sessionId() != null) { + sessionIds.add(response.sessionId()); + } + responses.add(response.body().asString()); + + try { + // need to sleep longer to give the session store some time to finish + // + // the operation to store session data into Redis is fired off when response headers are written, + // but there's nothing waiting for that operation to complete when the response is being sent + // + // therefore, if we send a 2nd request too quickly after receiving the 1st response, + // the session data may still be in the process of being stored and the 2nd request + // would get stale session data + Thread.sleep(500 + ThreadLocalRandom.current().nextInt(500)); + } catch (InterruptedException e) { + return; + } + } + } + + public void verify() { + assertEquals(1, sessionIds.size()); + String id = sessionIds.iterator().next(); + + assertEquals(requests, responses.size()); + int i = 1; + for (String response : responses) { + assertEquals(id + "|" + i, response); + i++; + } + } + } +} diff --git a/integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/websessions/CounterEndpoint.java b/integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/websessions/CounterEndpoint.java new file mode 100644 index 00000000000000..c31b79145ac9ba --- /dev/null +++ b/integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/websessions/CounterEndpoint.java @@ -0,0 +1,22 @@ +package io.quarkus.it.vertx.websessions; + +import io.quarkus.vertx.web.Route; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.Session; + +public class CounterEndpoint { + @Route(path = "/counter", methods = Route.HttpMethod.GET) + String counter(RoutingContext ctx) { + Session session = ctx.session(); + Integer counter = session.get("counter"); + counter = counter == null ? 1 : counter + 1; + session.put("counter", counter); + return session.id() + "|" + counter; + } + + @Route(path = "/check-sessions", methods = Route.HttpMethod.GET) + void checkSessions(RoutingContext ctx) { + Session session = ctx.session(); + ctx.end(session != null ? "OK" : "KO"); + } +} diff --git a/integration-tests/vertx-web/src/main/resources/application.properties b/integration-tests/vertx-web/src/main/resources/application.properties new file mode 100644 index 00000000000000..3f10c491bace20 --- /dev/null +++ b/integration-tests/vertx-web/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.http.sessions.mode=in-memory diff --git a/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/websessions/CounterTest.java b/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/websessions/CounterTest.java new file mode 100644 index 00000000000000..79acd3a66a5847 --- /dev/null +++ b/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/websessions/CounterTest.java @@ -0,0 +1,89 @@ +package io.quarkus.it.vertx.websessions; + +import static io.restassured.RestAssured.when; +import static io.restassured.RestAssured.with; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.filter.session.SessionFilter; +import io.restassured.response.Response; + +@QuarkusTest +public class CounterTest { + @Test + public void test() throws InterruptedException { + when().get("/check-sessions").then().statusCode(200).body(Matchers.is("OK")); + + List users = new ArrayList<>(); + for (int i = 0; i < 50; i++) { + users.add(new User(100)); + } + + for (User user : users) { + user.start(); + } + for (User user : users) { + user.join(); + } + for (User user : users) { + user.verify(); + } + } + + static class User extends Thread { + private static final AtomicInteger counter = new AtomicInteger(); + + private final Set sessionIds = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Queue responses = new ConcurrentLinkedQueue<>(); + + private final int requests; + + User(int requests) { + super("User" + counter.incrementAndGet()); + this.requests = requests; + } + + @Override + public void run() { + SessionFilter sessions = new SessionFilter(); + for (int i = 0; i < requests; i++) { + Response response = with().filter(sessions).get("/counter"); + if (response.sessionId() != null) { + sessionIds.add(response.sessionId()); + } + responses.add(response.body().asString()); + + try { + Thread.sleep(ThreadLocalRandom.current().nextInt(50)); + } catch (InterruptedException e) { + return; + } + } + } + + public void verify() { + assertEquals(1, sessionIds.size()); + String id = sessionIds.iterator().next(); + + assertEquals(requests, responses.size()); + int i = 1; + for (String response : responses) { + assertEquals(id + "|" + i, response); + i++; + } + } + } +}