From 902abfdae42685b68c127e309da17ec64ffd2382 Mon Sep 17 00:00:00 2001 From: Ryan Emerson Date: Tue, 22 Oct 2024 21:19:19 +0100 Subject: [PATCH] JDBC_PING as default discovery protocol Closes #29399 - Add ProviderFactory#dependsOn to allow dependencies between ProviderFactories to be explicitly defined - Disable Infinispan default shutdownhook disabled to ensure lifecycle is managed exclusively by Keycloak - Remove Infinispan shutdown hook in KeycloakRecorder and manage EmbeddedCacheManager lifecycle only in DefaultInfinispanConnectionProviderFactory#close Signed-off-by: Ryan Emerson Signed-off-by: Alexander Schwartz Co-authored-by: Alexander Schwartz --- .github/workflows/ci.yml | 11 ++ .../topics/changes/changes-26_1_0.adoc | 17 ++ docs/guides/server/caching.adoc | 16 +- model/infinispan/pom.xml | 4 + ...ltInfinispanConnectionProviderFactory.java | 19 +- ...inispanSingleUseObjectProviderFactory.java | 7 + .../META-INF/jpa-changelog-26.1.0.xml | 37 ++++ .../META-INF/jpa-changelog-master.xml | 1 + .../keycloak/config/database/Database.java | 30 +++- .../quarkus/deployment/CacheBuildSteps.java | 5 +- .../quarkus/deployment/KeycloakProcessor.java | 2 +- .../quarkus/runtime/KeycloakRecorder.java | 7 +- .../mappers/DatabasePropertyMappers.java | 8 + .../QuarkusKeycloakSessionFactory.java | 26 +-- .../infinispan/CacheManagerFactory.java | 86 +++++++--- .../QuarkusCacheManagerProvider.java | 5 +- .../runtime/src/main/resources/cache-ispn.xml | 2 +- .../configuration/test/ConfigurationTest.java | 8 +- .../cluster/ManagedCacheManagerProvider.java | 3 +- .../keycloak/provider/ProviderFactory.java | 13 ++ .../DefaultKeycloakSessionFactory.java | 162 ++++++++++++------ .../AbstractQuarkusDeployableContainer.java | 4 + 22 files changed, 336 insertions(+), 137 deletions(-) create mode 100644 model/jpa/src/main/resources/META-INF/jpa-changelog-26.1.0.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7053a1fafb..846f5ffefd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -596,6 +596,17 @@ jobs: echo "Tests: $TESTS" ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-quarkus -Pdb-${{ matrix.db }} "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -Dtest=$TESTS -pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh + - name: Run cluster JDBC_PING2 smoke test + run: | + ./mvnw test ${{ env.SUREFIRE_RETRY }} \ + -Pauth-server-cluster-quarkus \ + -Pdb-${{ matrix.db }} \ + -Dtest=RealmInvalidationClusterTest \ + -Dsession.cache.owners=2 \ + -Dauth.server.quarkus.cluster.stack=jdbc-ping \ + -pl testsuite/integration-arquillian/tests/base \ + 2>&1 | misc/log/trimmer.sh + - name: Upload JVM Heapdumps if: always() uses: ./.github/actions/upload-heapdumps diff --git a/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc index 67911da896..5206ef4604 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc @@ -28,3 +28,20 @@ To disable the virtual threads, add one of the Java system properties combinatio * `-Dorg.infinispan.threads.virtual=false`: disables virtual thread in both Infinispan and JGroups. * `-Djgroups.thread.virtual=false`: disables virtual threads only in JGroups. * `-Dorg.infinispan.threads.virtual=false -Djgroups.thread.virtual=true`: disables virtual threads only in Infinispan. + += Default transport stack changed to JDBC_PING2 for distributed caches + +Previous versions of {project_name} used as a default UDP multicast to discover other nodes to form a cluster and to synchronize the replicated caches of {project_name}. +This required multicast to be available and to be configured correctly, which is usually not the case in cloud environments. + +Starting with this version, the default changes to a configuration of JDBC_PING2 which uses {project_name}'s database to discover other nodes. +As this removes the need for multicast network capabilities, this is a simplification and a drop-in replacement. + +To enable the previous behavior, choose the transport stack `udp`. + +The {project_name} Operator will continue to configure `kubernetes` as a transport stack. + += Defining dependencies between provider factories + +When developing extensions for {project_name}, developers can now specify dependencies between provider factories classes by implementing the method `dependsOn()` in the `ProviderFactory` interface. +See the Javadoc for a detailed description. diff --git a/docs/guides/server/caching.adoc b/docs/guides/server/caching.adoc index c541a0825c..0d09e3960b 100644 --- a/docs/guides/server/caching.adoc +++ b/docs/guides/server/caching.adoc @@ -219,7 +219,7 @@ To apply a specific cache stack, enter this command: <@kc.start parameters="--cache-stack="/> -The default stack is set to `udp` when distributed caches are enabled. +The default stack is set to `jdbc-ping` when distributed caches are enabled. === Available transport stacks @@ -229,17 +229,19 @@ The following table shows transport stacks that are available without any furthe |=== |Stack name|Transport protocol|Discovery -|tcp|TCP|MPING (uses UDP multicast). -|udp|UDP|UDP multicast +|`tcp`|TCP|MPING (uses UDP multicast). +|`udp`|UDP|UDP multicast +|`jdbc-ping`|UDP|JDBC_PING2 |=== + The following table shows transport stacks that are available using the `--cache-stack` runtime option and a minimum configuration: [%autowidth] |=== |Stack name|Transport protocol|Discovery -|kubernetes|TCP|DNS_PING (requires `-Djgroups.dns.query=` to be added to JAVA_OPTS or JAVA_OPTS_APPEND environment variable). +|`kubernetes`|TCP|DNS_PING (requires `-Djgroups.dns.query=` to be added to JAVA_OPTS or JAVA_OPTS_APPEND environment variable). |=== === Additional transport stacks @@ -252,9 +254,9 @@ Instead, when you have a distributed cache setup running on AWS EC2 instances, y |=== |Stack name|Transport protocol|Discovery -|ec2|TCP|NATIVE_S3_PING -|google|TCP|GOOGLE_PING2 -|azure|TCP|AZURE_PING +|`ec2`|TCP|NATIVE_S3_PING +|`google`|TCP|GOOGLE_PING2 +|`azure`|TCP|AZURE_PING |=== Cloud vendor specific stacks have additional dependencies for {project_name}. diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml index 75afcc0a15..186e8da726 100755 --- a/model/infinispan/pom.xml +++ b/model/infinispan/pom.xml @@ -43,6 +43,10 @@ org.keycloak keycloak-model-storage + + org.keycloak + keycloak-model-jpa + org.keycloak keycloak-model-storage-private diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index b344c745ed..9247fc4a9f 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -20,6 +20,7 @@ package org.keycloak.connections.infinispan; import java.util.Arrays; import java.util.Iterator; import java.util.ServiceLoader; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; @@ -46,6 +47,7 @@ import org.keycloak.cluster.ClusterEvent; import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ManagedCacheManagerProvider; import org.keycloak.connections.infinispan.remote.RemoteInfinispanConnectionProvider; +import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.infinispan.util.InfinispanUtils; import org.keycloak.marshalling.Marshalling; import org.keycloak.models.KeycloakSession; @@ -56,6 +58,7 @@ import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.InvalidationHandler.ObjectType; +import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderEvent; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.ACTION_TOKEN_CACHE; @@ -112,7 +115,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon @Override public InfinispanConnectionProvider create(KeycloakSession session) { - lazyInit(); + lazyInit(session); return InfinispanUtils.isRemoteInfinispan() ? new RemoteInfinispanConnectionProvider(cacheManager, remoteCacheManager, topologyInfo) : @@ -160,15 +163,12 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon public void close() { logger.debug("Closing provider"); runWithWriteLockOnCacheManager(() -> { - if (cacheManager != null && !containerManaged) { + if (cacheManager != null) { cacheManager.stop(); } if (remoteCacheProvider != null) { remoteCacheProvider.stop(); } - if (remoteCacheManager != null && !containerManaged) { - remoteCacheManager.stop(); - } }); } @@ -191,7 +191,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon }); } - protected void lazyInit() { + protected void lazyInit(KeycloakSession keycloakSession) { if (cacheManager == null) { synchronized (this) { if (cacheManager == null) { @@ -207,7 +207,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon throw new RuntimeException("Multiple " + org.keycloak.cluster.ManagedCacheManagerProvider.class + " providers found."); } - managedCacheManager = provider.getEmbeddedCacheManager(config); + managedCacheManager = provider.getEmbeddedCacheManager(keycloakSession, config); if (InfinispanUtils.isRemoteInfinispan()) { rcm = provider.getRemoteCacheManager(config); } @@ -489,4 +489,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon } }); } + + @Override + public Set> dependsOn() { + return Set.of(JpaConnectionProvider.class); + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanSingleUseObjectProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanSingleUseObjectProviderFactory.java index e5944c322d..c003bdaa70 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanSingleUseObjectProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanSingleUseObjectProviderFactory.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -43,6 +44,7 @@ import org.keycloak.models.session.RevokedTokenPersisterProvider; import org.keycloak.models.sessions.infinispan.entities.SingleUseObjectValueEntity; import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.EnvironmentDependentProviderFactory; +import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ServerInfoAwareProviderFactory; @@ -65,6 +67,11 @@ public class InfinispanSingleUseObjectProviderFactory implements SingleUseObject private volatile boolean initialized; private boolean persistRevokedTokens; + @Override + public Set> dependsOn() { + return Set.of(InfinispanConnectionProvider.class); + } + @Override public InfinispanSingleUseObjectProvider create(KeycloakSession session) { initialize(session); diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.1.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.1.0.xml new file mode 100644 index 0000000000..cd77d9a321 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.1.0.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index e4fb81bc45..159c233af0 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -84,5 +84,6 @@ + diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java b/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java index af28909ef1..48c3f45e92 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/database/Database.java @@ -117,15 +117,15 @@ public final class Database { "org.h2.jdbcx.JdbcDataSource", "org.h2.Driver", "org.hibernate.dialect.H2Dialect", - new Function() { + new Function<>() { @Override public String apply(String alias) { if ("dev-file".equalsIgnoreCase(alias)) { - return addH2NonKeywords("jdbc:h2:file:${kc.home.dir:${kc.db-url-path:" + escapeReplacements(System.getProperty("user.home")) + "}}" + escapeReplacements(File.separator) + "${kc.data.dir:data}" - + escapeReplacements(File.separator) + "h2" + escapeReplacements(File.separator) - + "keycloakdb${kc.db-url-properties:}"); + return amendH2("jdbc:h2:file:${kc.home.dir:${kc.db-url-path:" + escapeReplacements(System.getProperty("user.home")) + "}}" + escapeReplacements(File.separator) + "${kc.data.dir:data}" + + escapeReplacements(File.separator) + "h2" + escapeReplacements(File.separator) + + "keycloakdb${kc.db-url-properties:}"); } - return addH2NonKeywords("jdbc:h2:mem:keycloakdb${kc.db-url-properties:}"); + return amendH2("jdbc:h2:mem:keycloakdb${kc.db-url-properties:}"); } private String escapeReplacements(String snippet) { @@ -155,6 +155,26 @@ public final class Database { } return jdbcUrl; } + + /** + * Required so that the H2 db instance is closed only when the Agroal connection pool is closed during + * Keycloak shutdown. We cannot rely on the default H2 ShutdownHook as this can result in the DB being + * closed before dependent resources, e.g. JDBC_PING2, are shutdown gracefully. This solution also + * requires the Agroal min-pool connection size to be at least 1. + */ + private String addH2CloseOnExit(String jdbcUrl) { + if (!jdbcUrl.contains("DB_CLOSE_ON_EXIT=")) { + jdbcUrl = jdbcUrl + ";DB_CLOSE_ON_EXIT=FALSE"; + } + if (!jdbcUrl.contains("DB_CLOSE_DELAY=")) { + jdbcUrl = jdbcUrl + ";DB_CLOSE_DELAY=0"; + } + return jdbcUrl; + } + + private String amendH2(String jdbcUrl) { + return addH2CloseOnExit(addH2NonKeywords(jdbcUrl)); + } }, asList("liquibase.database.core.H2Database"), "dev-mem", "dev-file" diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java index 24a6699ac6..3f82bcb6fe 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java @@ -23,7 +23,6 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.logging.LoggingSetupBuildItem; import jakarta.enterprise.context.ApplicationScoped; import org.keycloak.quarkus.runtime.KeycloakRecorder; @@ -40,11 +39,11 @@ public class CacheBuildSteps { @Consume(LoggingSetupBuildItem.class) @Record(ExecutionTime.RUNTIME_INIT) @BuildStep - void configureInfinispan(KeycloakRecorder recorder, BuildProducer syntheticBeanBuildItems, ShutdownContextBuildItem shutdownContext) { + void configureInfinispan(KeycloakRecorder recorder, BuildProducer syntheticBeanBuildItems) { syntheticBeanBuildItems.produce(SyntheticBeanBuildItem.configure(CacheManagerFactory.class) .scope(ApplicationScoped.class) .unremovable() .setRuntimeInit() - .runtimeValue(recorder.createCacheInitializer(shutdownContext)).done()); + .runtimeValue(recorder.createCacheInitializer()).done()); } } diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index f66d671000..770817c317 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -485,7 +485,7 @@ class KeycloakProcessor { } } - recorder.configSessionFactory(factories, defaultProviders, preConfiguredProviders, loadThemesFromClassPath(), Environment.isRebuild()); + recorder.configSessionFactory(factories, defaultProviders, preConfiguredProviders, loadThemesFromClassPath()); } private List loadThemesFromClassPath() { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index 3ccb568595..c2b9a92e4c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -121,14 +121,13 @@ public class KeycloakRecorder { Map, Map>>> factories, Map, String> defaultProviders, Map preConfiguredProviders, - List themes, boolean reaugmented) { - QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, themes, reaugmented)); + List themes) { + QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, themes)); } - public RuntimeValue createCacheInitializer(ShutdownContext shutdownContext) { + public RuntimeValue createCacheInitializer() { try { CacheManagerFactory cacheManagerFactory = new CacheManagerFactory(getInfinispanConfigFile()); - shutdownContext.addShutdownTask(cacheManagerFactory::shutdown); return new RuntimeValue<>(cacheManagerFactory); } catch (Exception e) { throw new RuntimeException(e); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index 63c6ded57c..88b413b051 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -69,6 +69,7 @@ final class DatabasePropertyMappers { .build(), fromOption(DatabaseOptions.DB_POOL_MIN_SIZE) .to("quarkus.datasource.jdbc.min-size") + .transformer(DatabasePropertyMappers::transformMinPoolSize) .paramLabel("size") .build(), fromOption(DatabaseOptions.DB_POOL_MAX_SIZE) @@ -118,4 +119,11 @@ final class DatabasePropertyMappers { return Database.getDialect(db).orElse(null); } + /** + * For H2 databases we must ensure that the min-pool size is at least one so that the DB is not shutdown until the + * Agroal connection pool is closed on Keycloak shutdown. + */ + private static String transformMinPoolSize(String min, ConfigSourceInterceptorContext context) { + return isDevModeDatabase(context) && (min == null || "0".equals(min)) ? "1" : min; + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java index 305e7d2e15..13ae7cdb53 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/QuarkusKeycloakSessionFactory.java @@ -17,7 +17,6 @@ package org.keycloak.quarkus.runtime.integration; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,20 +47,13 @@ public final class QuarkusKeycloakSessionFactory extends DefaultKeycloakSessionF } private static QuarkusKeycloakSessionFactory INSTANCE; - private final Boolean reaugmented; - private final Map, Map>>> factories; - private Map preConfiguredProviders; public QuarkusKeycloakSessionFactory( Map, Map>>> factories, Map, String> defaultProviders, Map preConfiguredProviders, - List themes, - Boolean reaugmented) { + List themes) { this.provider = defaultProviders; - this.factories = factories; - this.preConfiguredProviders = preConfiguredProviders; - this.reaugmented = reaugmented; serverStartupTimestamp = System.currentTimeMillis(); spis = factories.keySet(); @@ -88,25 +80,11 @@ public final class QuarkusKeycloakSessionFactory extends DefaultKeycloakSessionF } private QuarkusKeycloakSessionFactory() { - reaugmented = false; - factories = Collections.emptyMap(); } @Override public void init() { - // Component factory must be initialized first, so that postInit in other factories can use component factories - updateComponentFactoryProviderFactory(); - if (componentFactoryPF != null) { - componentFactoryPF.postInit(this); - } - for (Map f : factoriesMap.values()) { - for (ProviderFactory factory : f.values()) { - if (factory != componentFactoryPF) { - factory.postInit(this); - } - } - } - + initProviderFactories(); AdminPermissions.registerListener(this); // make the session factory ready for hot deployment ProviderManagerRegistry.SINGLETON.setDeployer(this); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java index b8ea6482b8..0edd03eb4e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java @@ -20,20 +20,26 @@ package org.keycloak.quarkus.runtime.storage.infinispan; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; import java.util.stream.Stream; +import io.agroal.api.AgroalDataSource; import io.micrometer.core.instrument.Metrics; +import io.quarkus.arc.Arc; +import jakarta.persistence.EntityManager; + import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCacheManager; import org.infinispan.client.hotrod.RemoteCacheManagerAdmin; import org.infinispan.client.hotrod.impl.ConfigurationProperties; -import org.infinispan.commons.api.Lifecycle; import org.infinispan.commons.dataconversion.MediaType; import org.infinispan.commons.internal.InternalCacheNames; import org.infinispan.commons.util.concurrent.CompletableFutures; @@ -42,6 +48,7 @@ import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.cache.HashConfiguration; import org.infinispan.configuration.cache.PersistenceConfigurationBuilder; import org.infinispan.configuration.global.GlobalConfiguration; +import org.infinispan.configuration.global.ShutdownHookBehavior; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.configuration.parsing.ParserRegistry; import org.infinispan.manager.DefaultCacheManager; @@ -50,8 +57,11 @@ import org.infinispan.persistence.remote.configuration.ExhaustedAction; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.infinispan.protostream.descriptors.FileDescriptor; import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants; +import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator; import org.infinispan.remoting.transport.jgroups.JGroupsTransport; import org.jboss.logging.Logger; +import org.jgroups.conf.ProtocolConfiguration; +import org.jgroups.protocols.JDBC_PING2; import org.jgroups.protocols.TCP_NIO2; import org.jgroups.protocols.UDP; import org.jgroups.util.TLS; @@ -60,10 +70,13 @@ import org.keycloak.common.Profile; import org.keycloak.common.util.MultiSiteUtils; import org.keycloak.config.CachingOptions; import org.keycloak.config.MetricsOptions; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.infinispan.util.InfinispanUtils; import org.keycloak.marshalling.KeycloakIndexSchemaUtil; import org.keycloak.marshalling.KeycloakModelSchema; import org.keycloak.marshalling.Marshalling; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.sessions.infinispan.query.ClientSessionQueries; import org.keycloak.models.sessions.infinispan.query.UserSessionQueries; import org.keycloak.models.sessions.infinispan.remote.RemoteInfinispanAuthenticationSessionProviderFactory; @@ -71,7 +84,9 @@ import org.keycloak.models.sessions.infinispan.remote.RemoteUserLoginFailureProv import org.keycloak.quarkus.runtime.configuration.Configuration; import javax.net.ssl.SSLContext; +import javax.sql.DataSource; +import static org.infinispan.configuration.global.TransportConfiguration.STACK; import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY; @@ -95,11 +110,12 @@ public class CacheManagerFactory { private static final Logger logger = Logger.getLogger(CacheManagerFactory.class); - private final CompletableFuture cacheManagerFuture; private final CompletableFuture remoteCacheManagerFuture; + private final String config; + private volatile DefaultCacheManager cacheManager; public CacheManagerFactory(String config) { - this.cacheManagerFuture = startEmbeddedCacheManager(config); + this.config = config; if (InfinispanUtils.isRemoteInfinispan()) { logger.debug("Remote Cache feature is enabled"); this.remoteCacheManagerFuture = CompletableFuture.supplyAsync(this::startRemoteCacheManager); @@ -109,20 +125,20 @@ public class CacheManagerFactory { } } - public DefaultCacheManager getOrCreateEmbeddedCacheManager() { - return join(cacheManagerFuture); + public DefaultCacheManager getOrCreateEmbeddedCacheManager(KeycloakSession keycloakSession) { + if (cacheManager == null) { + synchronized (this) { + if (cacheManager == null) + cacheManager = startEmbeddedCacheManager(keycloakSession); + } + } + return cacheManager; } public RemoteCacheManager getOrCreateRemoteCacheManager() { return join(remoteCacheManagerFuture); } - public void shutdown() { - logger.debug("Shutdown embedded and remote cache managers"); - cacheManagerFuture.thenAccept(CacheManagerFactory::close); - remoteCacheManagerFuture.thenAccept(CacheManagerFactory::close); - } - private static T join(Future future) { try { return future.get(getStartTimeout(), TimeUnit.SECONDS); @@ -134,12 +150,6 @@ public class CacheManagerFactory { } } - private static void close(Lifecycle lifecycle) { - if (lifecycle != null) { - lifecycle.stop(); - } - } - private RemoteCacheManager startRemoteCacheManager() { logger.info("Starting Infinispan remote cache manager (Hot Rod Client)"); String cacheRemoteHost = requiredStringProperty(CACHE_REMOTE_HOST_PROPERTY); @@ -278,10 +288,14 @@ public class CacheManagerFactory { admin.reindexCache(cacheName); } - private CompletableFuture startEmbeddedCacheManager(String config) { + private DefaultCacheManager startEmbeddedCacheManager(KeycloakSession keycloakSession) { logger.info("Starting Infinispan embedded cache manager"); ConfigurationBuilderHolder builder = new ParserRegistry().parse(config); + // We must disable the Infinispan default ShutdownHook as we manage the EmbeddedCacheManager lifecycle explicitly + // with #shutdown and multiple calls to EmbeddedCacheManager#stop can lead to Exceptions being thrown + builder.getGlobalConfigurationBuilder().shutdown().hookBehavior(ShutdownHookBehavior.DONT_REGISTER); + if (Configuration.isTrue(MetricsOptions.METRICS_ENABLED)) { builder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class); builder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry); @@ -310,7 +324,7 @@ public class CacheManagerFactory { } else { // embedded mode! if (builder.getNamedConfigurationBuilders().entrySet().stream().anyMatch(c -> c.getValue().clustering().cacheMode().isClustered())) { - configureTransportStack(builder); + configureTransportStack(builder, keycloakSession); configureRemoteStores(builder); } configureCacheMaxCount(builder, CachingOptions.CLUSTERED_MAX_COUNT_CACHES); @@ -320,8 +334,7 @@ public class CacheManagerFactory { configureCacheMaxCount(builder, CachingOptions.LOCAL_MAX_COUNT_CACHES); checkForRemoteStores(builder); - var start = isStartEagerly(); - return CompletableFuture.supplyAsync(() -> new DefaultCacheManager(builder, start)); + return new DefaultCacheManager(builder, isStartEagerly()); } private static boolean isRemoteTLSEnabled() { @@ -357,12 +370,37 @@ public class CacheManagerFactory { return Integer.getInteger("kc.cache-ispn-start-timeout", 120); } - private static void configureTransportStack(ConfigurationBuilderHolder builder) { + private void configureTransportStack(ConfigurationBuilderHolder builder, KeycloakSession keycloakSession) { String transportStack = Configuration.getRawValue("kc.cache-stack"); + var jdbcStackName = "jdbc-ping"; var transportConfig = builder.getGlobalConfigurationBuilder().transport(); - if (transportStack != null && !transportStack.isBlank()) { + var stackXmlAttribute = transportConfig.defaultTransport().attributes().attribute(STACK); + if (transportStack != null && !transportStack.isBlank() && !jdbcStackName.equals(transportStack)) { transportConfig.defaultTransport().stack(transportStack); + } else if (!stackXmlAttribute.isModified() || jdbcStackName.equals(stackXmlAttribute.get())){ + EntityManager em = keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager(); + var tableName = JpaUtils.getTableNameForNativeQuery("JGROUPS_PING", em); + var attributes = Map.of( + // Leave initialize_sql blank as table is already created by Keycloak + "initialize_sql", "", + // Explicitly specify clear and select_all SQL to ensure "cluster_name" column is used, as the default + // "cluster" cannot be used with Oracle DB as it's a reserved word. + "clear_sql", String.format("DELETE from %s WHERE cluster_name=?", tableName), + "delete_single_sql", String.format("DELETE from %s WHERE address=?", tableName), + "insert_single_sql", String.format("INSERT INTO %s values (?, ?, ?, ?, ?)", tableName), + "select_all_pingdata_sql", String.format("SELECT address, name, ip, coord FROM %s WHERE cluster_name=?", tableName), + "remove_all_data_on_view_change", "true", + "register_shutdown_hook", "false", + "stack.combine", "REPLACE", + "stack.position", "PING" + ); + var stack = List.of(new ProtocolConfiguration(JDBC_PING2.class.getSimpleName(), attributes)); + builder.addJGroupsStack(new EmbeddedJGroupsChannelConfigurator(jdbcStackName, stack, null), "udp"); + + Supplier dataSourceSupplier = Arc.container().select(AgroalDataSource.class)::get; + transportConfig.addProperty(JGroupsTransport.DATA_SOURCE, dataSourceSupplier); + transportConfig.defaultTransport().stack(jdbcStackName); } if (Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED_PROPERTY)) { @@ -378,7 +416,7 @@ public class CacheManagerFactory { .setClientAuth(TLSClientAuth.NEED) .setProtocols(new String[]{"TLSv1.3"}); transportConfig.addProperty(JGroupsTransport.SOCKET_FACTORY, tls.createSocketFactory()); - Logger.getLogger(CacheManagerFactory.class).info("MTLS enabled for communications for embedded caches"); + logger.info("MTLS enabled for communications for embedded caches"); } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java index 053325eb43..9527b9aa49 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java @@ -20,6 +20,7 @@ package org.keycloak.quarkus.runtime.storage.infinispan; import io.quarkus.arc.Arc; import org.keycloak.Config; import org.keycloak.cluster.ManagedCacheManagerProvider; +import org.keycloak.models.KeycloakSession; /** * @author Pedro Igor @@ -27,8 +28,8 @@ import org.keycloak.cluster.ManagedCacheManagerProvider; public final class QuarkusCacheManagerProvider implements ManagedCacheManagerProvider { @Override - public C getEmbeddedCacheManager(Config.Scope config) { - return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager(); + public C getEmbeddedCacheManager(KeycloakSession keycloakSession, Config.Scope config) { + return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager(keycloakSession); } @Override diff --git a/quarkus/runtime/src/main/resources/cache-ispn.xml b/quarkus/runtime/src/main/resources/cache-ispn.xml index 6044c02873..169e07d182 100644 --- a/quarkus/runtime/src/main/resources/cache-ispn.xml +++ b/quarkus/runtime/src/main/resources/cache-ispn.xml @@ -22,7 +22,7 @@ xmlns="urn:infinispan:config:15.0"> - + diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java index b47bb42f77..950a3dbed9 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java @@ -221,12 +221,12 @@ public class ConfigurationTest extends AbstractConfigurationTest { .toString() .replaceFirst(isWindows() ? "file:///" : "file://", ""); - assertEquals("jdbc:h2:file:" + userHomeUri + "data/h2/keycloakdb;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); + assertEquals("jdbc:h2:file:" + userHomeUri + "data/h2/keycloakdb;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=0", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); ConfigArgsConfigSource.setCliArgs("--db=dev-mem"); config = createConfig(); assertEquals(H2Dialect.class.getName(), config.getConfigValue("kc.db-dialect").getValue()); - assertEquals("jdbc:h2:mem:keycloakdb;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); + assertEquals("jdbc:h2:mem:keycloakdb;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=0", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); assertEquals("h2", config.getConfigValue("quarkus.datasource.db-kind").getValue()); ConfigArgsConfigSource.setCliArgs("--db=dev-mem", "--db-username=other"); @@ -304,13 +304,13 @@ public class ConfigurationTest extends AbstractConfigurationTest { ConfigArgsConfigSource.setCliArgs("--db=dev-file"); SmallRyeConfig config = createConfig(); assertEquals(H2Dialect.class.getName(), config.getConfigValue("kc.db-dialect").getValue()); - assertEquals("jdbc:h2:file:test-dir/data/h2/keycloakdb;;test=test;test1=test1;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); + assertEquals("jdbc:h2:file:test-dir/data/h2/keycloakdb;;test=test;test1=test1;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=0", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); assertEquals("xa", config.getConfigValue("quarkus.datasource.jdbc.transactions").getValue()); ConfigArgsConfigSource.setCliArgs(""); config = createConfig(); assertEquals(H2Dialect.class.getName(), config.getConfigValue("kc.db-dialect").getValue()); - assertEquals("jdbc:h2:file:test-dir/data/h2/keycloakdb;;test=test;test1=test1;NON_KEYWORDS=VALUE", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); + assertEquals("jdbc:h2:file:test-dir/data/h2/keycloakdb;;test=test;test1=test1;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=0", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); System.setProperty("kc.db-url-properties", "?test=test&test1=test1"); ConfigArgsConfigSource.setCliArgs("--db=mariadb"); diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java index 16bd66eac8..3f9cbb3c91 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java @@ -18,6 +18,7 @@ package org.keycloak.cluster; import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; /** * A Service Provider Interface (SPI) that allows to plug-in an embedded or remote cache manager instance. @@ -26,7 +27,7 @@ import org.keycloak.Config; */ public interface ManagedCacheManagerProvider { - C getEmbeddedCacheManager(Config.Scope config); + C getEmbeddedCacheManager(KeycloakSession keycloakSession, Config.Scope config); /** * @return A RemoteCacheManager if the features {@link org.keycloak.common.Profile.Feature#CLUSTERLESS} or {@link org.keycloak.common.Profile.Feature#MULTI_SITE} is enabled, {@code null} otherwise. diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java index 72a8170707..59e5ddf82f 100755 --- a/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/provider/ProviderFactory.java @@ -19,6 +19,8 @@ package org.keycloak.provider; import java.util.Collections; import java.util.List; +import java.util.Set; + import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -67,4 +69,15 @@ public interface ProviderFactory { default List getConfigMetadata() { return Collections.emptyList(); } + + /** + * Optional method used to declare that a ProviderFactory has a dependency on one or more Providers. If a Provider + * is declared here, it is guaranteed that the dependencies {@link #postInit} method will be executed + * before this ProviderFactory's {@link #postInit}. Similarly, it's guaranteed that {@link #close()} will be + * called on this {@link ProviderFactory} before {@link #close()} is called on any of the dependent ProviderFactory + * implementations. + */ + default Set> dependsOn() { + return Collections.emptySet(); + } } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index d0882e3ea2..ae73194b0d 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -16,6 +16,7 @@ */ package org.keycloak.services; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -24,8 +25,10 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.Stack; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; import java.util.stream.Stream; @@ -66,13 +69,6 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa // TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps protected long serverStartupTimestamp; - /** - * Timeouts are used as time boundary for obtaining models from an external storage. Default value is set - * to 3000 milliseconds and it's configurable. - */ - private Long clientStorageProviderTimeout; - private Long roleStorageProviderTimeout; - protected ComponentFactoryProviderFactory componentFactoryPF; @Override @@ -117,18 +113,7 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa } } checkProvider(); - // Component factory must be initialized first, so that postInit in other factories can use component factories - updateComponentFactoryProviderFactory(); - if (componentFactoryPF != null) { - componentFactoryPF.postInit(this); - } - for (Map factories : factoriesMap.values()) { - for (ProviderFactory factory : factories.values()) { - if (factory != componentFactoryPF) { - factory.postInit(this); - } - } - } + initProviderFactories(); // make the session factory ready for hot deployment ProviderManagerRegistry.SINGLETON.setDeployer(this); } @@ -136,6 +121,54 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa AdminPermissions.registerListener(this); } + protected void initProviderFactories() { + initProviderFactories(true, factoriesMap); + } + + protected void initProviderFactories(boolean updateComponentFactory, Map, Map> factories) { + if (updateComponentFactory) { + // Component factory must be initialized first, so that postInit in other factories can use component factories + updateComponentFactoryProviderFactory(); + if (componentFactoryPF != null) { + componentFactoryPF.postInit(this); + } + } + + Set> initializedProviders = new HashSet<>(); + Stack recursionPrevention = new Stack<>(); + + for(Map.Entry, Map> f : factories.entrySet()) { + if (initializedProviders.contains(f.getKey())) { + continue; + } + initializeProviders(f.getKey(), factories, initializedProviders, recursionPrevention); + } + } + + private void initializeProviders(Class provider, Map, Map> factories, Set> intializedProviders, Stack recursionPrevention) { + for (ProviderFactory factory : factories.get(provider).values()) { + if (factory == componentFactoryPF) + continue; + + for (Class providerDep : factory.dependsOn()) { + if (recursionPrevention.contains(factory)) { + List stackForException = recursionPrevention.stream().map(providerFactory -> providerFactory.getClass().getName()).toList(); + throw new RuntimeException("Detected a recursive dependency on provider " + providerDep.getName() + + " while the initialization of the following provider factories is ongoing: " + stackForException); + } + Map f = factories.get(providerDep); + if (f == null) { + throw new RuntimeException("No provider factories exists for provider " + providerDep.getSimpleName()); + } + recursionPrevention.push(factory); + initializeProviders(providerDep, factories, intializedProviders, recursionPrevention); + recursionPrevention.pop(); + } + factory.postInit(this); + intializedProviders.add(provider); + } + } + protected Map, Map> getFactoriesCopy() { Map, Map> copy = new HashMap<>(); for (Map.Entry, Map> entry : factoriesMap.entrySet()) { @@ -150,17 +183,22 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa public void deploy(ProviderManager pm) { Map, Map> copy = getFactoriesCopy(); Map, Map> newFactories = loadFactories(pm); - List deployed = new LinkedList<>(); + Map, Map> deployed = new HashMap<>(); List undeployed = new LinkedList<>(); for (Map.Entry, Map> entry : newFactories.entrySet()) { - Map current = copy.get(entry.getKey()); + Class provider = entry.getKey(); + Map current = copy.get(provider); if (current == null) { - copy.put(entry.getKey(), entry.getValue()); + copy.put(provider, entry.getValue()); } else { - for (ProviderFactory f : entry.getValue().values()) { - deployed.add(f); - ProviderFactory old = current.remove(f.getId()); + for (Map.Entry e : entry.getValue().entrySet()) { + deployed.compute(provider, (k, v) -> { + Map map = Objects.requireNonNullElseGet(v, HashMap::new); + map.put(e.getKey(), e.getValue()); + return map; + }); + ProviderFactory old = current.remove(e.getValue().getId()); if (old != null) { undeployed.add(old); } @@ -178,18 +216,7 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa factory.close(); cfChanged |= (componentFactoryPF == factory); } - // Component factory must be initialized first, so that postInit in other factories can use component factories - if (cfChanged) { - updateComponentFactoryProviderFactory(); - if (componentFactoryPF != null) { - componentFactoryPF.postInit(this); - } - } - for (ProviderFactory factory : deployed) { - if (factory != componentFactoryPF) { - factory.postInit(this); - } - } + initProviderFactories(cfChanged, deployed); if (pm.getInfo().hasThemes() || pm.getInfo().hasThemeResources()) { themeManagerFactory.clearCache(); @@ -415,11 +442,52 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa @Override public void close() { ProviderManagerRegistry.SINGLETON.setDeployer(null); - for (Map factories : factoriesMap.values()) { - for (ProviderFactory factory : factories.values()) { - factory.close(); + + // Create a tree-structure to represent reverse relation of ProviderFactory#dependsOn to Providers + Map, Node>> nodes = new HashMap<>(); + for (Map.Entry, Map> f : factoriesMap.entrySet()) { + Class provider = f.getKey(); + for (Map.Entry entry : f.getValue().entrySet()) { + ProviderFactory pf = entry.getValue(); + Node> node = nodes.computeIfAbsent(provider, k -> new Node<>(new HashSet<>())); + // Add ProviderFactory to the associated Provider node + node.data.add(pf); + // If dependencies exist, make this node a child of the Provider dependencies node so that we can ensure + // that the leaves of the tree are closed first + pf.dependsOn().forEach(dep -> { + node.parent = nodes.computeIfAbsent((Class) dep, k -> new Node<>(new HashSet<>())); + node.parent.children.add(node); + }); } } + nodes.values().forEach(this::closeProvider); + } + + private void closeProvider(Node> node) { + for (var it = node.children.iterator(); it.hasNext(); ) { + closeProvider(it.next()); + it.remove(); + } + + // Provider has no other dependent ProviderFactories, it's ProviderFactories can safely be closed + for (var it = node.data.iterator(); it.hasNext(); ) { + ProviderFactory pf = it.next(); + logger.debugf("Closing ProviderFactory: %s", pf.getClass().getName()); + pf.close(); + it.remove(); + } + } + + private static class Node { + private final T data; + private Node parent; + private List> children; + + public Node(T data) { + this.data = data; + this.parent = null; + this.children = new ArrayList<>(); + } } public static boolean isInternal(ProviderFactory factory) { @@ -427,20 +495,6 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa return packageName.startsWith("org.keycloak") && !packageName.startsWith("org.keycloak.examples"); } - public long getClientStorageProviderTimeout() { - if (clientStorageProviderTimeout == null) { - clientStorageProviderTimeout = Config.scope("client").getLong("storageProviderTimeout", 3000L); - } - return clientStorageProviderTimeout; - } - - public long getRoleStorageProviderTimeout() { - if (roleStorageProviderTimeout == null) { - roleStorageProviderTimeout = Config.scope("role").getLong("storageProviderTimeout", 3000L); - } - return roleStorageProviderTimeout; - } - /** * @return timestamp of Keycloak server startup */ diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java index 17f2199a6d..13e81928fd 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java @@ -195,6 +195,10 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo } else { commands.add("--cache=ispn"); commands.add("--cache-config-file=cluster-" + cacheMode + ".xml"); + + var stack = System.getProperty("auth.server.quarkus.cluster.stack"); + if (stack != null) + commands.add("--cache-stack=" + stack); } log.debugf("FIPS Mode: %s", configuration.getFipsMode());