From f3d0a0d63a110a29011a8519baf784576246a2f5 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Fri, 13 Oct 2023 16:41:09 +0200 Subject: [PATCH 1/4] Infinispan Client: improve dev services logging --- .../devservices/InfinispanDevServiceProcessor.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java index f5e94548cdf32..44c09b6e1b228 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java @@ -159,9 +159,7 @@ private void runInfinispanDevService(String clientName, Map newDevServices, Map properties) { try { - log.infof("Starting Dev Service for connection %s", clientName); InfinispanDevServicesConfig namedDevServiceConfig = config.devService.devservices; - log.infof("Apply Dev Services config %s", namedDevServiceConfig); RunningDevService devService = startContainer(clientName, dockerStatusBuildItem, namedDevServiceConfig, launchMode.getLaunchMode(), !devServicesSharedNetworkBuildItem.isEmpty(), globalDevServicesConfig.timeout, properties); @@ -189,7 +187,7 @@ private RunningDevService startContainer(String clientName, DockerStatusBuildIte } String configPrefix = getConfigPrefix(clientName); - log.info("Config prefix " + configPrefix); + log.debugf("Config prefix %s", configPrefix); boolean needToStart = !ConfigUtils.isPropertyPresent(configPrefix + "hosts") && !ConfigUtils.isPropertyPresent(configPrefix + "server-list"); @@ -205,6 +203,9 @@ private RunningDevService startContainer(String clientName, DockerStatusBuildIte return null; } + log.infof("Starting Dev Service for connection %s", clientName); + log.debugf("Apply Dev Services config %s", devServicesConfig); + Supplier infinispanServerSupplier = () -> { QuarkusInfinispanContainer infinispanContainer = new QuarkusInfinispanContainer(clientName, devServicesConfig, launchMode, From 1d55b6cce8aa756cf072bb46660377398c223405 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 11 Oct 2023 16:06:18 +0200 Subject: [PATCH 2/4] Infinispan Client: add runtime SPI and deployment SPI modules Some constants from the Infinispan Client runtime module are moved to the runtime SPI module, so that they can be used from other modules that cannot directly depend on the Infinispan Client extension. Two build items, `InfinispanClientBuildItem` and `InfinispanClientNameBuildItem`, are moved from the Infinispan Client deployment module to the deployment SPI module. This is technically a breaking change, because they are also moved to a different package, but these build items don't seem to be used anywhere outside of Quarkus, so it should be safe. --- bom/application/pom.xml | 10 +++++ .../infinispan-client/deployment-spi/pom.xml | 44 +++++++++++++++++++ .../spi}/InfinispanClientBuildItem.java | 5 +-- .../spi}/InfinispanClientNameBuildItem.java | 2 +- .../infinispan-client/deployment/pom.xml | 4 ++ .../InfinispanBindingProcessor.java | 1 + .../deployment/InfinispanClientProcessor.java | 8 ++-- extensions/infinispan-client/pom.xml | 2 + .../infinispan-client/runtime-spi/pom.xml | 15 +++++++ .../runtime/spi/InfinispanConstants.java | 6 +++ extensions/infinispan-client/runtime/pom.xml | 8 ++-- .../client/runtime/InfinispanClientUtil.java | 6 ++- 12 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 extensions/infinispan-client/deployment-spi/pom.xml rename extensions/infinispan-client/{deployment/src/main/java/io/quarkus/infinispan/client/deployment => deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi}/InfinispanClientBuildItem.java (87%) rename extensions/infinispan-client/{deployment/src/main/java/io/quarkus/infinispan/client/deployment => deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi}/InfinispanClientNameBuildItem.java (88%) create mode 100644 extensions/infinispan-client/runtime-spi/pom.xml create mode 100644 extensions/infinispan-client/runtime-spi/src/main/java/io/quarkus/infinispan/client/runtime/spi/InfinispanConstants.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 3b33db87b0c04..9d955c9737abc 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1224,6 +1224,16 @@ quarkus-infinispan-client ${project.version} + + io.quarkus + quarkus-infinispan-client-runtime-spi + ${project.version} + + + io.quarkus + quarkus-infinispan-client-deployment-spi + ${project.version} + io.quarkus quarkus-infinispan-client-deployment diff --git a/extensions/infinispan-client/deployment-spi/pom.xml b/extensions/infinispan-client/deployment-spi/pom.xml new file mode 100644 index 0000000000000..63fa74e0f5c84 --- /dev/null +++ b/extensions/infinispan-client/deployment-spi/pom.xml @@ -0,0 +1,44 @@ + + + + quarkus-infinispan-client-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-infinispan-client-deployment-spi + Quarkus - Infinispan - Client - Deployment SPI + + + + io.quarkus + quarkus-infinispan-client-runtime-spi + + + io.quarkus + quarkus-core-deployment + + + 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 + + + + + + diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientBuildItem.java b/extensions/infinispan-client/deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi/InfinispanClientBuildItem.java similarity index 87% rename from extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientBuildItem.java rename to extensions/infinispan-client/deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi/InfinispanClientBuildItem.java index 13c0e8fb70167..ed658d82d79cc 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientBuildItem.java +++ b/extensions/infinispan-client/deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi/InfinispanClientBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.infinispan.client.deployment; +package io.quarkus.infinispan.client.deployment.spi; import org.infinispan.client.hotrod.RemoteCacheManager; @@ -12,8 +12,7 @@ public final class InfinispanClientBuildItem extends MultiBuildItem { private final RuntimeValue client; private final String name; - public InfinispanClientBuildItem(RuntimeValue client, - String name) { + public InfinispanClientBuildItem(RuntimeValue client, String name) { this.client = client; this.name = name; } diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientNameBuildItem.java b/extensions/infinispan-client/deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi/InfinispanClientNameBuildItem.java similarity index 88% rename from extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientNameBuildItem.java rename to extensions/infinispan-client/deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi/InfinispanClientNameBuildItem.java index 95481aa42144a..64f10f004234f 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientNameBuildItem.java +++ b/extensions/infinispan-client/deployment-spi/src/main/java/io/quarkus/infinispan/client/deployment/spi/InfinispanClientNameBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.infinispan.client.deployment; +package io.quarkus.infinispan.client.deployment.spi; import io.quarkus.builder.item.MultiBuildItem; diff --git a/extensions/infinispan-client/deployment/pom.xml b/extensions/infinispan-client/deployment/pom.xml index 163f3a0896b32..e4aa11df67fa5 100644 --- a/extensions/infinispan-client/deployment/pom.xml +++ b/extensions/infinispan-client/deployment/pom.xml @@ -29,6 +29,10 @@ io.quarkus quarkus-infinispan-client + + io.quarkus + quarkus-infinispan-client-deployment-spi + io.quarkus quarkus-caffeine-deployment diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanBindingProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanBindingProcessor.java index 78b22f6058d7b..3ecfe43a080da 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanBindingProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanBindingProcessor.java @@ -7,6 +7,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.infinispan.client.deployment.spi.InfinispanClientBuildItem; import io.quarkus.infinispan.client.runtime.InfinispanClientUtil; import io.quarkus.kubernetes.service.binding.spi.ServiceBindingQualifierBuildItem; diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java index 58201a89f0b4f..bebfff5f64fb1 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java @@ -92,6 +92,8 @@ import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.infinispan.client.InfinispanClientName; import io.quarkus.infinispan.client.Remote; +import io.quarkus.infinispan.client.deployment.spi.InfinispanClientBuildItem; +import io.quarkus.infinispan.client.deployment.spi.InfinispanClientNameBuildItem; import io.quarkus.infinispan.client.runtime.InfinispanClientBuildTimeConfig; import io.quarkus.infinispan.client.runtime.InfinispanClientProducer; import io.quarkus.infinispan.client.runtime.InfinispanClientUtil; @@ -623,9 +625,9 @@ List infinispanClients(InfinispanRecorder recorder, List infinispanClientNames, // make sure all beans have been initialized @SuppressWarnings("unused") BeanContainerBuildItem beanContainer) { - List result = new ArrayList<>(infinispanClientNames.size()); - for (InfinispanClientNameBuildItem ic : infinispanClientNames) { - String name = ic.getName(); + Set names = infinispanClientNames.stream().map(icn -> icn.getName()).collect(Collectors.toSet()); + List result = new ArrayList<>(names.size()); + for (String name : names) { result.add(new InfinispanClientBuildItem(recorder.getClient(name), name)); } return result; diff --git a/extensions/infinispan-client/pom.xml b/extensions/infinispan-client/pom.xml index 098c9701f3126..4bcaaeebab119 100644 --- a/extensions/infinispan-client/pom.xml +++ b/extensions/infinispan-client/pom.xml @@ -15,6 +15,8 @@ pom deployment + deployment-spi runtime + runtime-spi diff --git a/extensions/infinispan-client/runtime-spi/pom.xml b/extensions/infinispan-client/runtime-spi/pom.xml new file mode 100644 index 0000000000000..32c2c6571463b --- /dev/null +++ b/extensions/infinispan-client/runtime-spi/pom.xml @@ -0,0 +1,15 @@ + + + + quarkus-infinispan-client-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-infinispan-client-runtime-spi + Quarkus - Infinispan - Client - Runtime SPI + + diff --git a/extensions/infinispan-client/runtime-spi/src/main/java/io/quarkus/infinispan/client/runtime/spi/InfinispanConstants.java b/extensions/infinispan-client/runtime-spi/src/main/java/io/quarkus/infinispan/client/runtime/spi/InfinispanConstants.java new file mode 100644 index 0000000000000..bb139bb217d05 --- /dev/null +++ b/extensions/infinispan-client/runtime-spi/src/main/java/io/quarkus/infinispan/client/runtime/spi/InfinispanConstants.java @@ -0,0 +1,6 @@ +package io.quarkus.infinispan.client.runtime.spi; + +public class InfinispanConstants { + public static final String DEFAULT_INFINISPAN_CLIENT_NAME = ""; + public static final String INFINISPAN_CLIENT_CONFIG_ROOT_NAME = "infinispan-client"; +} diff --git a/extensions/infinispan-client/runtime/pom.xml b/extensions/infinispan-client/runtime/pom.xml index a9ff61b97ae81..91440a2169ae3 100644 --- a/extensions/infinispan-client/runtime/pom.xml +++ b/extensions/infinispan-client/runtime/pom.xml @@ -41,6 +41,10 @@ io.quarkus quarkus-elytron-security-common + + io.quarkus + quarkus-infinispan-client-runtime-spi + org.wildfly.security wildfly-elytron-sasl-plain @@ -75,9 +79,7 @@ org.jboss.spec.javax.transaction - - jboss-transaction-api_1.2_spec - + jboss-transaction-api_1.2_spec io.netty diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientUtil.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientUtil.java index 99c6b72c42c69..14d7496dd39ab 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientUtil.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientUtil.java @@ -3,11 +3,13 @@ import java.util.Collection; import java.util.List; +import io.quarkus.infinispan.client.runtime.spi.InfinispanConstants; + public final class InfinispanClientUtil { public static final String DEFAULT_INFINISPAN_DEV_SERVICE_NAME = "infinispan"; - public static final String DEFAULT_INFINISPAN_CLIENT_NAME = ""; - public static final String INFINISPAN_CLIENT_CONFIG_ROOT_NAME = "infinispan-client"; + public static final String DEFAULT_INFINISPAN_CLIENT_NAME = InfinispanConstants.DEFAULT_INFINISPAN_CLIENT_NAME; + public static final String INFINISPAN_CLIENT_CONFIG_ROOT_NAME = InfinispanConstants.INFINISPAN_CLIENT_CONFIG_ROOT_NAME; public static boolean isDefault(String infinispanClientName) { return DEFAULT_INFINISPAN_CLIENT_NAME.equals(infinispanClientName); From e5d73a7f0f0bdbceba2f557bb7dfb2c0b21e916d Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 11 Oct 2023 16:12:21 +0200 Subject: [PATCH 3/4] Redis Client: add runtime SPI and deployment SPI modules Some constants from the Redis Client runtime module are moved to the runtime SPI module, so that they can be used from other modules that cannot directly depend on the Redis Client extension. One build item, `RequestedRedisClientBuildItem`, is moved from the Redis Client deployment module to the deployment SPI module. This is technically a breaking change, because it is also moved to a different package, but this build item doesn't seem to be used anywhere outside of Quarkus, so it should be safe. Further, one new build item, `RedisClientBuildItem`, is added. It provides runtime access to the Redis clients (in the Mutiny variant) without having to perform a CDI lookup. --- bom/application/pom.xml | 10 +++++++ .../redis/deployment/RedisCacheProcessor.java | 2 +- .../redis-client/deployment-spi/pom.xml | 30 +++++++++++++++++++ .../client/spi/RedisClientBuildItem.java | 27 +++++++++++++++++ .../spi}/RequestedRedisClientBuildItem.java | 2 +- extensions/redis-client/deployment/pom.xml | 4 +++ .../client/RedisClientProcessor.java | 8 ++++- .../client/RedisDatasourceProcessor.java | 1 + extensions/redis-client/pom.xml | 2 ++ extensions/redis-client/runtime-spi/pom.xml | 15 ++++++++++ .../redis/runtime/spi/RedisConstants.java | 7 +++++ extensions/redis-client/runtime/pom.xml | 4 +++ .../runtime/client/config/RedisConfig.java | 7 +++-- 13 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 extensions/redis-client/deployment-spi/pom.xml create mode 100644 extensions/redis-client/deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi/RedisClientBuildItem.java rename extensions/redis-client/{deployment/src/main/java/io/quarkus/redis/deployment/client => deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi}/RequestedRedisClientBuildItem.java (86%) create mode 100644 extensions/redis-client/runtime-spi/pom.xml create mode 100644 extensions/redis-client/runtime-spi/src/main/java/io/quarkus/redis/runtime/spi/RedisConstants.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 9d955c9737abc..2cc9c890209e5 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -5972,6 +5972,11 @@ quarkus-redis-client ${project.version} + + io.quarkus + quarkus-redis-client-runtime-spi + ${project.version} + io.quarkus quarkus-redis-cache @@ -5983,6 +5988,11 @@ quarkus-redis-client-deployment ${project.version} + + io.quarkus + quarkus-redis-client-deployment-spi + ${project.version} + io.quarkus quarkus-redis-cache-deployment diff --git a/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java b/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java index 4de5b1628d3f4..2325b45656a24 100644 --- a/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java +++ b/extensions/redis-cache/deployment/src/main/java/io/quarkus/cache/redis/deployment/RedisCacheProcessor.java @@ -34,7 +34,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.redis.deployment.client.RequestedRedisClientBuildItem; +import io.quarkus.redis.deployment.client.spi.RequestedRedisClientBuildItem; import io.quarkus.redis.runtime.client.config.RedisConfig; import io.smallrye.mutiny.Uni; diff --git a/extensions/redis-client/deployment-spi/pom.xml b/extensions/redis-client/deployment-spi/pom.xml new file mode 100644 index 0000000000000..236a49220a79c --- /dev/null +++ b/extensions/redis-client/deployment-spi/pom.xml @@ -0,0 +1,30 @@ + + + + quarkus-redis-client-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-redis-client-deployment-spi + Quarkus - Redis Client - Deployment SPI + + + + io.quarkus + quarkus-redis-client-runtime-spi + + + io.quarkus + quarkus-core-deployment + + + io.smallrye.reactive + smallrye-mutiny-vertx-redis-client + + + + diff --git a/extensions/redis-client/deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi/RedisClientBuildItem.java b/extensions/redis-client/deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi/RedisClientBuildItem.java new file mode 100644 index 0000000000000..dae9a2d7a64ad --- /dev/null +++ b/extensions/redis-client/deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi/RedisClientBuildItem.java @@ -0,0 +1,27 @@ +package io.quarkus.redis.deployment.client.spi; + +import java.util.function.Supplier; + +import io.quarkus.builder.item.MultiBuildItem; +import io.vertx.mutiny.redis.client.Redis; + +/** + * Provides runtime access to the Redis clients, in the Mutiny variant. + */ +public final class RedisClientBuildItem extends MultiBuildItem { + private final Supplier client; + private final String name; + + public RedisClientBuildItem(Supplier client, String name) { + this.client = client; + this.name = name; + } + + public Supplier getClient() { + return client; + } + + public String getName() { + return name; + } +} diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RequestedRedisClientBuildItem.java b/extensions/redis-client/deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi/RequestedRedisClientBuildItem.java similarity index 86% rename from extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RequestedRedisClientBuildItem.java rename to extensions/redis-client/deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi/RequestedRedisClientBuildItem.java index a35af3572769c..4dc1b3ee5604e 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RequestedRedisClientBuildItem.java +++ b/extensions/redis-client/deployment-spi/src/main/java/io/quarkus/redis/deployment/client/spi/RequestedRedisClientBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.redis.deployment.client; +package io.quarkus.redis.deployment.client.spi; import io.quarkus.builder.item.MultiBuildItem; diff --git a/extensions/redis-client/deployment/pom.xml b/extensions/redis-client/deployment/pom.xml index e18a963cfe2c4..f267e7df63797 100644 --- a/extensions/redis-client/deployment/pom.xml +++ b/extensions/redis-client/deployment/pom.xml @@ -31,6 +31,10 @@ io.quarkus quarkus-redis-client + + io.quarkus + quarkus-redis-client-deployment-spi + io.quarkus quarkus-smallrye-health-spi diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java index 55308b824e85d..bdaeed05a6a53 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java @@ -48,6 +48,8 @@ import io.quarkus.redis.client.RedisHostsProvider; import io.quarkus.redis.client.RedisOptionsCustomizer; import io.quarkus.redis.client.reactive.ReactiveRedisClient; +import io.quarkus.redis.deployment.client.spi.RedisClientBuildItem; +import io.quarkus.redis.deployment.client.spi.RequestedRedisClientBuildItem; import io.quarkus.redis.runtime.client.RedisClientRecorder; import io.quarkus.redis.runtime.client.config.RedisConfig; import io.quarkus.runtime.LaunchMode; @@ -127,7 +129,8 @@ public void init( VertxBuildItem vertxBuildItem, ApplicationArchivesBuildItem applicationArchivesBuildItem, LaunchModeBuildItem launchMode, BuildProducer nativeImageResources, - BuildProducer hotDeploymentWatchedFiles) { + BuildProducer hotDeploymentWatchedFiles, + BuildProducer clientSuppliers) { // Collect the used redis clients, the unused clients will not be instantiated. Set names = new HashSet<>(); @@ -177,6 +180,9 @@ public void init( .produce(configureAndCreateSyntheticBean(name, RedisClient.class, recorder.getLegacyRedisClient(name))); syntheticBeans.produce(configureAndCreateSyntheticBean(name, ReactiveRedisClient.class, recorder.getLegacyReactiveRedisClient(name))); + + // build items + clientSuppliers.produce(new RedisClientBuildItem(recorder.getRedisClient(name), name)); } recorder.cleanup(shutdown); diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java index a9c8679b768a2..9944fe65a9ced 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java @@ -30,6 +30,7 @@ import io.quarkus.redis.datasource.ReactiveRedisDataSource; import io.quarkus.redis.datasource.RedisDataSource; import io.quarkus.redis.datasource.codecs.Codec; +import io.quarkus.redis.deployment.client.spi.RequestedRedisClientBuildItem; import io.quarkus.redis.runtime.client.RedisClientRecorder; import io.quarkus.vertx.deployment.VertxBuildItem; diff --git a/extensions/redis-client/pom.xml b/extensions/redis-client/pom.xml index 458b2563a75bf..c861fb139cc9f 100644 --- a/extensions/redis-client/pom.xml +++ b/extensions/redis-client/pom.xml @@ -18,7 +18,9 @@ deployment + deployment-spi runtime + runtime-spi diff --git a/extensions/redis-client/runtime-spi/pom.xml b/extensions/redis-client/runtime-spi/pom.xml new file mode 100644 index 0000000000000..49ac428649924 --- /dev/null +++ b/extensions/redis-client/runtime-spi/pom.xml @@ -0,0 +1,15 @@ + + + + quarkus-redis-client-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-redis-client-runtime-spi + Quarkus - Redis Client - Runtime SPI + + diff --git a/extensions/redis-client/runtime-spi/src/main/java/io/quarkus/redis/runtime/spi/RedisConstants.java b/extensions/redis-client/runtime-spi/src/main/java/io/quarkus/redis/runtime/spi/RedisConstants.java new file mode 100644 index 0000000000000..d810e83009f32 --- /dev/null +++ b/extensions/redis-client/runtime-spi/src/main/java/io/quarkus/redis/runtime/spi/RedisConstants.java @@ -0,0 +1,7 @@ +package io.quarkus.redis.runtime.spi; + +public class RedisConstants { + public static final String REDIS_CONFIG_ROOT_NAME = "redis"; + public static final String HOSTS_CONFIG_NAME = "hosts"; + public static final String DEFAULT_CLIENT_NAME = ""; +} diff --git a/extensions/redis-client/runtime/pom.xml b/extensions/redis-client/runtime/pom.xml index f00b8e2c6715e..3dd920a374e64 100644 --- a/extensions/redis-client/runtime/pom.xml +++ b/extensions/redis-client/runtime/pom.xml @@ -27,6 +27,10 @@ io.smallrye.reactive smallrye-mutiny-vertx-redis-client + + io.quarkus + quarkus-redis-client-runtime-spi + io.quarkus diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java index ac13188c6338b..f6b2171d6c060 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisConfig.java @@ -2,6 +2,7 @@ import java.util.Map; +import io.quarkus.redis.runtime.spi.RedisConstants; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -12,9 +13,9 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public interface RedisConfig { - public final static String REDIS_CONFIG_ROOT_NAME = "redis"; - public final static String HOSTS_CONFIG_NAME = "hosts"; - public static final String DEFAULT_CLIENT_NAME = ""; + public final static String REDIS_CONFIG_ROOT_NAME = RedisConstants.REDIS_CONFIG_ROOT_NAME; + public final static String HOSTS_CONFIG_NAME = RedisConstants.HOSTS_CONFIG_NAME; + public static final String DEFAULT_CLIENT_NAME = RedisConstants.DEFAULT_CLIENT_NAME; /** * The default redis client From e6e973a67ecc75b1fbb1a4889d4dbff0d48c7944 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 11 Oct 2023 16:14:59 +0200 Subject: [PATCH 4/4] Add support for Vert.x Web sessions --- bom/application/pom.xml | 20 +++ devtools/bom-descriptor-json/pom.xml | 26 ++++ docs/pom.xml | 26 ++++ docs/src/main/asciidoc/http-reference.adoc | 146 +++++++++++++++++- extensions/infinispan-client/pom.xml | 3 + extensions/infinispan-client/runtime/pom.xml | 5 + .../sessions/deployment/pom.xml | 52 +++++++ .../InfinispanSessionsBuildTimeConfig.java | 23 +++ .../InfinispanSessionsProcessor.java | 46 ++++++ .../sessions/runtime/pom.xml | 85 ++++++++++ .../runtime/InfinispanSessionsConfig.java | 28 ++++ .../runtime/InfinispanSessionsRecorder.java | 38 +++++ .../resources/META-INF/quarkus-extension.yaml | 13 ++ extensions/redis-client/pom.xml | 3 + extensions/redis-client/runtime/pom.xml | 5 + .../redis-client/sessions/deployment/pom.xml | 52 +++++++ .../RedisSessionsBuildTimeConfig.java | 20 +++ .../deployment/RedisSessionsProcessor.java | 46 ++++++ .../redis-client/sessions/runtime/pom.xml | 61 ++++++++ .../sessions/runtime/RedisSessionsConfig.java | 21 +++ .../runtime/RedisSessionsRecorder.java | 32 ++++ .../resources/META-INF/quarkus-extension.yaml | 13 ++ extensions/vertx-http/deployment/pom.xml | 2 +- .../SessionStoreProviderBuildItem.java | 24 +++ .../http/deployment/VertxHttpProcessor.java | 47 +++++- .../http/runtime/CurrentVertxRequest.java | 11 ++ .../http/runtime/HttpBuildTimeConfig.java | 6 + .../vertx/http/runtime/HttpConfiguration.java | 5 + .../vertx/http/runtime/RouteConstants.java | 6 +- .../http/runtime/SessionsBuildTimeConfig.java | 46 ++++++ .../vertx/http/runtime/SessionsConfig.java | 98 ++++++++++++ .../http/runtime/SessionsInMemoryConfig.java | 37 +++++ .../vertx/http/runtime/VertxHttpRecorder.java | 55 ++++++- .../client/websessions/CounterResource.java | 21 +++ .../src/main/resources/application.properties | 1 + .../client/websessions/CounterTest.java | 93 +++++++++++ .../redis/it/websessions/CounterResource.java | 21 +++ .../src/main/resources/application.properties | 8 +- .../redis/it/websessions/CounterTest.java | 93 +++++++++++ .../it/vertx/websessions/CounterEndpoint.java | 22 +++ .../src/main/resources/application.properties | 1 + .../it/vertx/websessions/CounterTest.java | 89 +++++++++++ 42 files changed, 1441 insertions(+), 9 deletions(-) create mode 100644 extensions/infinispan-client/sessions/deployment/pom.xml create mode 100644 extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsBuildTimeConfig.java create mode 100644 extensions/infinispan-client/sessions/deployment/src/main/java/io/quarkus/infinispan/sessions/deployment/InfinispanSessionsProcessor.java create mode 100644 extensions/infinispan-client/sessions/runtime/pom.xml create mode 100644 extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsConfig.java create mode 100644 extensions/infinispan-client/sessions/runtime/src/main/java/io/quarkus/infinispan/sessions/runtime/InfinispanSessionsRecorder.java create mode 100644 extensions/infinispan-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 extensions/redis-client/sessions/deployment/pom.xml create mode 100644 extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsBuildTimeConfig.java create mode 100644 extensions/redis-client/sessions/deployment/src/main/java/io/quarkus/redis/sessions/deployment/RedisSessionsProcessor.java create mode 100644 extensions/redis-client/sessions/runtime/pom.xml create mode 100644 extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsConfig.java create mode 100644 extensions/redis-client/sessions/runtime/src/main/java/io/quarkus/redis/sessions/runtime/RedisSessionsRecorder.java create mode 100644 extensions/redis-client/sessions/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/SessionStoreProviderBuildItem.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java create mode 100644 integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/websessions/CounterResource.java create mode 100644 integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/websessions/CounterTest.java create mode 100644 integration-tests/redis-client/src/main/java/io/quarkus/redis/it/websessions/CounterResource.java create mode 100644 integration-tests/redis-client/src/test/java/io/quarkus/redis/it/websessions/CounterTest.java create mode 100644 integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/websessions/CounterEndpoint.java create mode 100644 integration-tests/vertx-web/src/main/resources/application.properties create mode 100644 integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/websessions/CounterTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 2cc9c890209e5..f649747bbf143 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 @@ -5977,6 +5987,11 @@ quarkus-redis-client-runtime-spi ${project.version} + + io.quarkus + quarkus-redis-client-sessions + ${project.version} + io.quarkus quarkus-redis-cache @@ -5993,6 +6008,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 cbdbc050e4dfc..8dfa44fda4f1c 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 dbf502dbfae57..01e3333970ccb 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 100241d911879..06d26b87906a8 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -546,7 +546,150 @@ link:https://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html#predicates If you are using a `web.xml` file as your configuration file, you can place it in the `src/main/resources/META-INF` directory. -=== Built-in route order values +[[vertx-web-sessions]] +== Sessions + +Quarkus includes support for sessions, based on https://vertx.io/docs/vertx-web/java/#_handling_sessions[Vert.x Web sessions]. + +By default, sessions are disabled. +To enable them, set the `quarkus.http.sessions.mode` configuration property to: + +`in-memory`:: for sessions stored in memory of the Quarkus application +`redis`:: for sessions stored in an external Redis server; requires using the Quarkus Redis Client extension +`infinispan`:: for sessions stored in an external Infinispan data grid; requires using the Quarkus Infinispan Client extension + +This configuration property is fixed at build time and cannot be changed at runtime. + +WARNING: Undertow includes its own support for sessions. +If Undertow is present, Vert.x Web sessions cannot be enabled. + +Sessions require using a cookie, which holds the session identifier. +By default, the cookie name is `JSESSIONID`. +Cookieless sessions are not supported. +Storing session data directly into the session cookie is not supported either. + +=== Accessing sessions + +When sessions are enabled, the Vert.x Web `Session` object may be obtained from the current `RoutingContext` using `RoutingContext.session()`. +Alternatively, the `io.vertx.ext.web.Session` object may be injected. + +When using non-clustered in-memory sessions, arbitrary objects may be stored into a session. + +With Redis, Infinispan, or cluster-wide in-memory sessions, the following data types may be stored into a session: + +* primitive wrapper types +** `java.lang.Boolean` +** `java.lang.Byte` +** `java.lang.Short` +** `java.lang.Integer` +** `java.lang.Long` +** `java.lang.Float` +** `java.lang.Double` +** `java.lang.Character` +* `java.lang.String` +* `byte[]` +* `io.vertx.core.buffer.Buffer` +* `io.vertx.core.json.JsonObject` +* `io.vertx.core.json.JsonArray` + +Session data are stored in a _session store_. +At the beginning of request processing, session data are loaded from the session store and put into the `Session` object. +The `Session` object contains an independent snapshot of session data and can be accessed freely during request processing. +When the response is being written, session data are stored back to the session store. + +Session data are loaded and stored as an entire whole, which means that they are always internally consistent. +In case of concurrent access to the session, the initial session state (at the beginning of a request) is identical to the final session state of some previous request. +There is no guarantee that modifications will survive (another conflicting write may win the race). + +[WARNING] +==== +In case of non-clustered in-memory sessions, requests share the `Session` object and the instances stored in it. +It is your responsibility as an application programmer to properly synchronize access to these shared data. + +With Redis or Infinispan, the `Session` objects are truly independent snapshots. +There are no objects shared between requests in this case. +==== + +Loading and storing session data in a fine-grained fashion (per individual attribute) is not supported, which makes reasoning about concurrent session access easier. +It also means you need to pay attention to the session size. + +Persisting session data back to the session store is initiated when response headers are written, but the operation is otherwise asynchronous to response body writing. +It is possible that persisting the session to the session store takes longer than writing the entire response, especially in case of tiny responses. + +=== General configuration + +include::{generated-dir}/config/quarkus-vertx-http-config-group-sessions-build-time-config.adoc[leveloffset=+1, opts=optional] + +include::{generated-dir}/config/quarkus-vertx-http-config-group-sessions-config.adoc[leveloffset=+1, opts=optional] + +=== In-memory sessions + +When `quarkus.http.sessions.mode` is set to `in-memory`, session data are stored in a Vert.x shared map. +By default, that shared map is _local_, which means that the session data are stored only in the JVM heap of the Quarkus application. + +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 configured, in-memory sessions may also be configured to be _cluster-wide_. +In this mode, a Vert.x _cluster-wide_ shared map is used to store session data, which means that sticky sessions are not necessary and losing one replica doesn't lead to session data loss. + +include::{generated-dir}/config/quarkus-http-sessions-in-memory-sessions-in-memory-config.adoc[leveloffset=+1, opts=optional] + +=== Redis sessions + +When `quarkus.http.sessions.mode` is set to `redis`, session data are stored in an external Redis server. +The xref:./redis.adoc[Quarkus Redis Client] extension must be present and a connection to the Redis server used to store session data must be configured there. + +By default, the default (unnamed) Redis connection is used. +To select a different (named) Redis connection, set the `quarkus.http.sessions.redis.client-name` configuration property. +For example: + +[source,properties] +---- +quarkus.http.sessions.mode=redis +quarkus.http.sessions.redis.client-name=web-sessions <1> + +quarkus.redis.web-sessions.hosts=redis://localhost:6379/7 <2> +---- +<1> Use the `web-sessions` Redis client for storing session data. +<2> Use database `7` on the Redis server at `localhost:6379`. + +The Redis-based session store requires an entire Redis database for itself. +When using a standalone Redis server, you can use a https://redis.io/commands/select/[logical database] that is not used for other purposes. +If you want to store session data into a Redis cluster, you need to dedicate an entire cluster, because Redis cluster only supports database zero. + +include::{generated-dir}/config/quarkus-redis-sessions.adoc[leveloffset=+1, opts=optional] + +=== Infinispan sessions + +When `quarkus.http.sessions.mode` is set to `infinispan`, session data are stored in an external Infinispan data grid. +The xref:./infinispan-client.adoc[Quarkus Infinispan Client] extension must be present and a connection to the Infinispan data grid used to store session data must be configured there. + +By default, the default (unnamed) Infinispan connection is used. +To select a different (named) Infinispan connection, set the `quarkus.http.sessions.infinispan.client-name` configuration property. +For example: + +[source,properties] +---- +quarkus.http.sessions.mode=infinispan +quarkus.http.sessions.infinispan.client-name=web-sessions <1> + +quarkus.infinispan-client.web-sessions.hosts=localhost:11222 <2> +---- +<1> Use the `web-sessions` Infinispan client for storing session data. +<2> Use the Infinispan data grid at `localhost:11222`. + +By default, the Infinispan cache used for storing session data is called `quarkus.sessions`. +To use a different cache, set the `quarkus.http.sessions.infinispan.cache-name` configuration property. + +The Infinispan-based session store verifies if the configured cache exists. +If it does not, it is created automatically from the `DIST_SYNC` default template. +Therefore, the Infinispan client must be configured to connect as a user with permissions equivalent to at least the `deployer` Infinispan role. + +include::{generated-dir}/config/quarkus-infinispan-sessions.adoc[leveloffset=+1, opts=optional] + +== Built-in route order values Route order values are the values that are specified via Vert.x route `io.vertx.ext.web.Route.order(int)` function. @@ -565,6 +708,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 4bcaaeebab119..771ef999e2409 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 91440a2169ae3..adc82bcf5e15b 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 0000000000000..ee7cdc70de254 --- /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 0000000000000..52f17f892d0ec --- /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 connect as a user with the necessary permissions + * on the Infinispan server. The required minimum is equivalent to 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 0000000000000..111c4c8b5c0b5 --- /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 0000000000000..fd9d9dd2dad3b --- /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 0000000000000..56c332f2d1b36 --- /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 0000000000000..c82b315dfb0a7 --- /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 0000000000000..476e398efc5ae --- /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 c861fb139cc9f..543702fa281ba 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 3dd920a374e64..2ae90adb17c23 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 0000000000000..16b78ba733b58 --- /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 0000000000000..caf5991dcba04 --- /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 0000000000000..2d6f3091ac120 --- /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 0000000000000..6490629ee0e02 --- /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 0000000000000..defd5be8cded9 --- /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 0000000000000..3608e74acc464 --- /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 0000000000000..4f4ef6cbe8e9b --- /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 ed3dd67a09031..8d3eff0acb0e0 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 0000000000000..d3a9a87fc1389 --- /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 fd8ec2b3ce36e..233f14893dcd1 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 217c01185a875..85d077e91ae4f 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 c1a2819bd3a88..99d2f1e6a8ebc 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 89ffdf53d0c19..749c01bad42c6 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 6d00a3afa9b07..dd8c286dc94f7 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 0000000000000..204de777d1e9a --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsBuildTimeConfig.java @@ -0,0 +1,46 @@ +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, in-memory sessions may be configured to be stored + * cluster-wide, 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 0000000000000..c53fbbdbf1693 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsConfig.java @@ -0,0 +1,98 @@ +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. + *

    + *
  • {@code always}: the session cookie always has the {@code Secure} attribute
  • + *
  • {@code never}: the session cookie never has the {@code Secure} attribute
  • + *
  • {@code auto}: 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 + *
+ */ + @ConfigItem(defaultValue = "auto") + public SessionCookieSecure cookieSecure; + + /** + * The value of the {@code SameSite} attribute of the session cookie. + * By default, the {@code SameSite} attribute is not present. + */ + @ConfigItem + public Optional cookieSameSite; + + /** + * 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; + + 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 0000000000000..b2592bf2c0afd --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/SessionsInMemoryConfig.java @@ -0,0 +1,37 @@ +package io.quarkus.vertx.http.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 memory. + */ +@ConfigRoot(name = "http.sessions.in-memory", phase = ConfigPhase.RUN_TIME) +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 stored cluster-wide. + *

+ * Ignored when Vert.x clustering is not enabled. + */ + @ConfigItem(defaultValue = "false") + public boolean clusterWide; + + /** + * 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 cluster-wide. + */ + @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 fbcb893f48b9d..bd50392bc2633 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 { @@ -187,18 +192,25 @@ private boolean uriValid(HttpServerRequest httpServerRequest) { final HttpBuildTimeConfig httpBuildTimeConfig; final ManagementInterfaceBuildTimeConfig managementBuildTimeConfig; final RuntimeValue httpConfiguration; + final RuntimeValue inMemorySessionsConfiguration; final RuntimeValue managementConfiguration; private static volatile Handler managementRouter; + final RuntimeValue vertxConfiguration; + public VertxHttpRecorder(HttpBuildTimeConfig httpBuildTimeConfig, ManagementInterfaceBuildTimeConfig managementBuildTimeConfig, RuntimeValue httpConfiguration, - RuntimeValue managementConfiguration) { + RuntimeValue inMemorySessionsConfiguration, + RuntimeValue managementConfiguration, + RuntimeValue vertxConfiguration) { this.httpBuildTimeConfig = httpBuildTimeConfig; this.httpConfiguration = httpConfiguration; + this.inMemorySessionsConfiguration = inMemorySessionsConfiguration; this.managementBuildTimeConfig = managementBuildTimeConfig; this.managementConfiguration = managementConfiguration; + this.vertxConfiguration = vertxConfiguration; } public static void setHotReplacement(Handler handler, HotReplacementContext hrc) { @@ -346,6 +358,23 @@ public void mountFrameworkRouter(RuntimeValue mainRouter, RuntimeValue createInMemorySessionStore() { + return new Supplier() { + @Override + public SessionStore get() { + Vertx vertx = VertxCoreRecorder.getVertx().get(); + SessionsInMemoryConfig config = inMemorySessionsConfiguration.getValue(); + if (config.clusterWide + && vertxConfiguration.getValue().cluster() != null + && vertxConfiguration.getValue().cluster().clustered()) { + return ClusteredSessionStore.create(vertx, config.mapName, config.retryTimeout.toMillis()); + } else { + return LocalSessionStore.create(vertx, config.mapName); + } + } + }; + } + public void finalizeRouter(BeanContainer container, Consumer defaultRouteHandler, List filterList, List managementInterfaceFilterList, Supplier vertx, LiveReloadConfig liveReloadConfig, Optional> mainRouterRuntimeValue, @@ -355,7 +384,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 +442,28 @@ 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 cookiePath; + if (sessions.path.isEmpty() || "/".equals(sessions.path)) { + cookiePath = rootPath.endsWith("/") ? rootPath.substring(0, rootPath.length() - 1) : rootPath; + } else { + cookiePath = rootPath + + (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(cookiePath) + .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 0000000000000..d5f008f62cb1b --- /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 d9e2391da36e8..d1dfb6129874e 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 0000000000000..28cfe99241e1f --- /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 0000000000000..ce4e905d1bb00 --- /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 6bb245e908d5e..caaa993568770 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 0000000000000..0de4a5b639809 --- /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 0000000000000..c31b79145ac9b --- /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 0000000000000..3f10c491bace2 --- /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 0000000000000..79acd3a66a584 --- /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++; + } + } + } +}