diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc index bb58ea78e1d..26ddb9b5641 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc @@ -77,6 +77,13 @@ For more information about brokering, see link:{developerguide_link}#_identity_b Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}. It also lists significant changes to internal APIs. +=== Endpoints are opened while Keycloak is initializing + +By default, {project_name} now opens its HTTP(S) and Management ports while initialization is still in progress. +If you use a proxy or load balancer, configure an HTTP health check with the path `/health/ready` to ensure traffic is only routed to the server once it is fully ready. + +If this behavior is not desired or an HTTP health check is not possible, start {project_name} with `--server-async-bootstrap=false` to revert to the previous behavior where ports are opened only after initialization completes. + === Dev Mode defaults to localhost When running the server in dev mode on a platform other than Windows Subsystem For Linux, the `http-host` setting will default to localhost. diff --git a/docs/guides/observability/health.adoc b/docs/guides/observability/health.adoc index 4221eca7a40..2ff073fe4c0 100644 --- a/docs/guides/observability/health.adoc +++ b/docs/guides/observability/health.adoc @@ -34,12 +34,16 @@ These endpoints respond with HTTP status `200 OK` on success or `503 Service Una } ---- -.Successful response for endpoints with information on the database connection: +.Successful response for endpoints with additional per-check information: [source, json] ---- { "status": "UP", "checks": [ + { + "name": "Keycloak Initialized", + "status": "UP" + }, { "name": "Graceful Shutdown", "status": "UP" @@ -128,6 +132,10 @@ The table below shows the available checks. |Will start to return "DOWN" once the pre-shutdown phase started. |No +|Keycloak Initialized +|Returns the status of the server initialization. +|No + |=== For some checks, you'll need to also enable metrics as indicated by the *Requires Metrics* column. To enable metrics diff --git a/docs/guides/server/configuration-production.adoc b/docs/guides/server/configuration-production.adoc index cdc7846bf45..dc6f1c8a438 100644 --- a/docs/guides/server/configuration-production.adoc +++ b/docs/guides/server/configuration-production.adoc @@ -52,6 +52,20 @@ By default, there is no limit set. Set the option `http-max-queued-requests` to limit the number of queued requests to a given threshold matching your environment. Any request that exceeds this limit would return with an immediate `503 Server not Available` response. +== Server bootstrap behavior + +Server initialization, such as database migrations, may take a significant amount of time to complete. +By default, {project_name} opens its HTTP(S) and Management endpoints while initialization is still in progress in the background. +This allows the startup and liveness probes to report UP early, preventing orchestrators like Kubernetes from killing the container during a long-running migration, while the readiness probe reports DOWN until initialization completes. + +If you run {project_name} behind a proxy or load balancer, configure an HTTP health check with the path `/health/ready` to ensure traffic is routed only to instances that have completed initialization. + +If an HTTP health check is not possible, or you prefer the server to accept connections only after initialization completes, start {project_name} with the following option: + +<@kc.start parameters="--server-async-bootstrap=false"/> + +With this setting, {project_name} opens its endpoints only after the bootstrap is complete and the server is ready to handle requests. + == Production grade database The database used by {project_name} is crucial for the overall performance, availability, reliability and integrity of {project_name}. For details on how to configure a supported database, see <@links.server id="db"/>. diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java index 7c2b5896060..1b96e97b56d 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java @@ -25,6 +25,7 @@ public enum OptionCategory { EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED), IMPORT("Import", 140, ConfigSupportLevel.SUPPORTED), OPENAPI("OpenAPI configuration", 150, ConfigSupportLevel.SUPPORTED), + SERVER("Server configuration", 160, ConfigSupportLevel.SUPPORTED), BOOTSTRAP_ADMIN("Bootstrap Admin", 998, ConfigSupportLevel.SUPPORTED), GENERAL("General", 999, ConfigSupportLevel.SUPPORTED); diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/ServerOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/ServerOptions.java new file mode 100644 index 00000000000..a7e02560d73 --- /dev/null +++ b/quarkus/config-api/src/main/java/org/keycloak/config/ServerOptions.java @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.config; + +public final class ServerOptions { + + private ServerOptions() {} + + public static final Option SERVER_ASYNC_BOOTSTRAP = new OptionBuilder<>("server-async-bootstrap", Boolean.class) + .category(OptionCategory.SERVER) + .defaultValue(Boolean.TRUE) + .description("If true, endpoints are opened while the bootstrap runs in the background. If false, endpoints are opened after bootstrap completes, ensuring the server is ready to handle requests.") + .build(); +} 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 9c5d11366d4..f9a27887ccf 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 @@ -96,6 +96,7 @@ import org.keycloak.quarkus.runtime.configuration.mappers.WildcardPropertyMapper import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakHandlerChainCustomizer; import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakTracingCustomizer; import org.keycloak.quarkus.runtime.logging.ClearMappedDiagnosticContextFilter; +import org.keycloak.quarkus.runtime.services.health.BoostrapReadyHealthCheck; import org.keycloak.quarkus.runtime.services.health.KeycloakClusterReadyHealthCheck; import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck; import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory; @@ -838,6 +839,7 @@ class KeycloakProcessor { if (isHealthDisabled()) { disableReadyHealthCheck(removeBeans, index); disableClusterHealthCheck(removeBeans, index); + disableBootstrapReadyHealthCheck(removeBeans, index); return; } if (isMetricsDisabled()) { @@ -860,6 +862,11 @@ class KeycloakProcessor { removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean.asClass(), false)); } + private static void disableBootstrapReadyHealthCheck(BuildProducer removeBeans, CombinedIndexBuildItem index) { + ClassInfo disabledBean = index.getIndex().getClassByName(DotName.createSimple(BoostrapReadyHealthCheck.class.getName())); + removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean.asClass(), false)); + } + @BuildStep void disableMdcContextFilter(BuildProducer removeBeans, CombinedIndexBuildItem index) { if (!Configuration.isTrue(LoggingOptions.LOG_MDC_ENABLED)) { diff --git a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/MetricsEnabledProfile.java b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/MetricsEnabledProfile.java index ffe1836978c..7764f101867 100644 --- a/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/MetricsEnabledProfile.java +++ b/quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/MetricsEnabledProfile.java @@ -32,8 +32,9 @@ public class MetricsEnabledProfile implements QuarkusTestProfile { "kc.health-enabled","true", "kc.metrics-enabled", "true", "kc.cache", "local", + "kc.server-async-bootstrap", "false", "quarkus.micrometer.export.prometheus.path", "/prom/metrics", "quarkus.class-loading.removed-artifacts", "io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-oracle-deployment"); // config works a bit odd in unit tests, so this is to ensure we exclude Oracle to avoid ClassNotFound ex } -} \ No newline at end of file +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java index b9a62efffd3..c8ddad00fee 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java @@ -54,7 +54,8 @@ public final class PropertyMappers { new FeaturePropertyMappers(), new ImportPropertyMappers(), new ManagementPropertyMappers(), new MetricsPropertyMappers(), new OpenApiPropertyMappers(), new LoggingPropertyMappers(), new ProxyPropertyMappers(), new VaultPropertyMappers(), new TracingPropertyMappers(), new TransactionPropertyMappers(), - new SecurityPropertyMappers(), new TruststorePropertyMappers(), new TelemetryPropertyMappers()); + new SecurityPropertyMappers(), new TruststorePropertyMappers(), new TelemetryPropertyMappers(), + new ServerPropertyMappers()); } public static List getPropertyMapperGroupings() { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ServerPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ServerPropertyMappers.java new file mode 100644 index 00000000000..a07947760da --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ServerPropertyMappers.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.configuration.mappers; + +import java.util.List; + +import org.keycloak.config.ServerOptions; + +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; + +public final class ServerPropertyMappers implements PropertyMapperGrouping { + @Override + public List> getPropertyMappers() { + return List.of( + fromOption(ServerOptions.SERVER_ASYNC_BOOTSTRAP) + .paramLabel("enabled") + .build() + ); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java index f922bd82a3b..7a27d22dd51 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java @@ -21,9 +21,9 @@ import jakarta.enterprise.event.Observes; import jakarta.ws.rs.ApplicationPath; import org.keycloak.config.BootstrapAdminOptions; +import org.keycloak.config.ServerOptions; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; @@ -41,9 +41,12 @@ import io.quarkus.runtime.StartupEvent; import io.smallrye.common.annotation.Blocking; import org.jboss.logging.Logger; +import static org.keycloak.common.util.Environment.isDevMode; +import static org.keycloak.common.util.Environment.isNonServerMode; + @ApplicationPath("/") @Blocking -public class QuarkusKeycloakApplication extends KeycloakApplication { +public class QuarkusKeycloakApplication extends KeycloakApplication { private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN"; private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD"; @@ -72,10 +75,13 @@ public class QuarkusKeycloakApplication extends KeycloakApplication { } @Override - public KeycloakSessionFactory createSessionFactory() { - QuarkusKeycloakSessionFactory instance = QuarkusKeycloakSessionFactory.getInstance(); - instance.init(); - return instance; + public QuarkusKeycloakSessionFactory createSessionFactory() { + return QuarkusKeycloakSessionFactory.getInstance(); + } + + @Override + protected void initKeycloakSessionFactory(QuarkusKeycloakSessionFactory quarkusKeycloakSessionFactory) { + quarkusKeycloakSessionFactory.init(); } @Override @@ -105,7 +111,16 @@ public class QuarkusKeycloakApplication extends KeycloakApplication { } @Override - protected int getTransactionTimeout(KeycloakSessionFactory sessionFactory) { + protected boolean supportsAsyncInitialization() { + var asyncBootstrap = Configuration.getOptionalKcValue(ServerOptions.SERVER_ASYNC_BOOTSTRAP) + .map(Boolean::parseBoolean) + .orElse(Boolean.TRUE); + // skip async bootstrap in dev and non-server mode + return !isDevMode() && !isNonServerMode() && asyncBootstrap; + } + + @Override + protected int getTransactionTimeout(QuarkusKeycloakSessionFactory sessionFactory) { return ((QuarkusJpaConnectionProviderFactory) sessionFactory.getProviderFactory(JpaConnectionProvider.class)).getMigrationTransactionTimeout(); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/BootstrapFilter.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/BootstrapFilter.java new file mode 100644 index 00000000000..be75760a6fa --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/BootstrapFilter.java @@ -0,0 +1,49 @@ +package org.keycloak.quarkus.runtime.services; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.keycloak.services.resources.KeycloakApplication; + +import org.jboss.resteasy.reactive.server.ServerRequestFilter; + +/** + * Pre-matching request filter that returns a 503 Service Unavailable response while the server bootstrap is in progress. + */ +@ApplicationScoped +public class BootstrapFilter { + + private final long startup; + private boolean ready; + + public BootstrapFilter() { + startup = System.currentTimeMillis(); + } + + @ServerRequestFilter(priority = 1, preMatching = true) + public Response filter(ContainerRequestContext ignored) { + if (ready) { + // JVM branch prediction may optimize this code and saves on reading a static volatile field + return null; + } + if (KeycloakApplication.isBootstrapCompleted()) { + // Return null to continue the request chain normally + ready = true; + return null; + } + // Implement a back-off to wait as long as the current start-up took, but then retry at least once per minute + long retry = Math.min(Math.max((System.currentTimeMillis() - startup) / 1000, 1), 60); + // Return 503 Service Unavailable + return Response + .status(Response.Status.SERVICE_UNAVAILABLE) + .type(MediaType.TEXT_PLAIN) + .entity("Boostrap in progress. Retry in " + retry + " seconds.") + .header(HttpHeaders.RETRY_AFTER, retry) + .header("Refresh", retry) + .build(); + + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/BoostrapReadyHealthCheck.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/BoostrapReadyHealthCheck.java new file mode 100644 index 00000000000..3a36d6956ce --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/BoostrapReadyHealthCheck.java @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.services.health; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.keycloak.services.resources.KeycloakApplication; + +import io.smallrye.health.api.AsyncHealthCheck; +import io.smallrye.mutiny.Uni; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; +import org.eclipse.microprofile.health.Readiness; + +/** + * Readiness health check that reports DOWN while the server bootstrap is in progress and UP once initialization completes. + */ +@Readiness +@ApplicationScoped +public class BoostrapReadyHealthCheck implements AsyncHealthCheck { + + private static final HealthCheckResponse UP = builder().up().build(); + private boolean bootstrapCompleted; + + @Override + public Uni call() { + // JVM branch prediction may optimize this code and saves on reading a static volatile field + if (bootstrapCompleted) { + return ready(); + } + if (KeycloakApplication.isBootstrapCompleted()) { + bootstrapCompleted = true; + return ready(); + } + return Uni.createFrom().item(builder().down().build()); + } + + private Uni ready() { + return Uni.createFrom().item(UP); + } + + private static HealthCheckResponseBuilder builder() { + return HealthCheckResponse.named("Keycloak Initialized"); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheck.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheck.java index adfbf8635a5..13d89620bd4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheck.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheck.java @@ -19,10 +19,7 @@ package org.keycloak.quarkus.runtime.services.health; import java.time.Instant; import java.util.concurrent.atomic.AtomicReference; -import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory; -import org.keycloak.infinispan.util.InfinispanUtils; -import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; import io.smallrye.health.api.AsyncHealthCheck; import io.smallrye.mutiny.Uni; @@ -34,15 +31,15 @@ import static org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCh public class KeycloakClusterReadyHealthCheck implements AsyncHealthCheck { private final AtomicReference failingSince = new AtomicReference<>(); + private final InfinispanConnectionProviderFactory factory; + + public KeycloakClusterReadyHealthCheck(InfinispanConnectionProviderFactory factory) { + this.factory = factory; + } @Override public Uni call() { var builder = HealthCheckResponse.named("Keycloak cluster health check").up(); - if (InfinispanUtils.isRemoteInfinispan()) { - return Uni.createFrom().item(builder.build()); - } - var sessionFactory = QuarkusKeycloakSessionFactory.getInstance(); - InfinispanConnectionProviderFactory factory = (InfinispanConnectionProviderFactory) sessionFactory.getProviderFactory(InfinispanConnectionProvider.class); if (factory.isClusterHealthy()) { failingSince.set(null); } else { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheckProducer.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheckProducer.java index 926797d3422..7a22d2e78b6 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheckProducer.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheckProducer.java @@ -22,7 +22,7 @@ import jakarta.enterprise.inject.Produces; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory; -import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; +import org.keycloak.services.resources.KeycloakApplication; import io.smallrye.health.api.AsyncHealthCheck; import org.eclipse.microprofile.health.Readiness; @@ -30,16 +30,32 @@ import org.eclipse.microprofile.health.Readiness; @ApplicationScoped public class KeycloakClusterReadyHealthCheckProducer { + private AsyncHealthCheck instance; + private boolean ready; + @Produces @Readiness @Dependent public AsyncHealthCheck createHealthCheck() { - var sessionFactory = QuarkusKeycloakSessionFactory.getInstance(); - InfinispanConnectionProviderFactory factory = (InfinispanConnectionProviderFactory) sessionFactory.getProviderFactory(InfinispanConnectionProvider.class); - if (factory.isClusterHealthSupported()) { - return new KeycloakClusterReadyHealthCheck(); - } else { + if (ready) { + // JVM branch prediction may optimize this code and saves on reading a static volatile field + return instance; + } + if (!KeycloakApplication.isBootstrapCompleted()) { return null; } + synchronized (this) { + if (ready) { + return instance; + } + var sessionFactory = KeycloakApplication.getSessionFactory(); + var factory = (InfinispanConnectionProviderFactory) sessionFactory.getProviderFactory(InfinispanConnectionProvider.class); + if (factory.isClusterHealthSupported()) { + instance = new KeycloakClusterReadyHealthCheck(factory); + } + ready = true; + } + + return instance; } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java index 9200e3aceec..934c6e1a2b1 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java @@ -83,7 +83,7 @@ public class HealthDistTest { .statusCode(200); when().get("/health/ready").then() .statusCode(200) - .body("checks.size()", equalTo(3)); + .body("checks.size()", equalTo(4)); when().get("/lb-check").then() .statusCode(404); } diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt index 341dd30f859..de2a66b533f 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt @@ -269,4 +269,11 @@ Truststore: in a container environment. Default: true. --truststore-paths List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or - directories containing those files that will be used as a system truststore. \ No newline at end of file + directories containing those files that will be used as a system truststore. + +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt index f120675eee1..3bf45628496 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt @@ -271,4 +271,11 @@ Truststore: in a container environment. Default: true. --truststore-paths List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or - directories containing those files that will be used as a system truststore. \ No newline at end of file + directories containing those files that will be used as a system truststore. + +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. \ No newline at end of file diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt index 401be1e8a14..034d887053e 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt @@ -279,6 +279,13 @@ Export: Set the number of users per file. It is used only if 'users' is set to 'different_files'. Default: 50. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt index 113a048eed0..62a57ffd986 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt @@ -569,6 +569,13 @@ OpenAPI configuration: available at '/openapi/ui'. Default: false. Available only when OpenAPI Endpoint is enabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt index ac4686b203a..418e1ed424d 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt @@ -274,6 +274,13 @@ Import: Set if existing data should be overwritten. If set to false, data will be ignored. Default: true. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt index 303beb5b71e..a6c92078cb6 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt @@ -564,6 +564,13 @@ OpenAPI configuration: available at '/openapi/ui'. Default: false. Available only when OpenAPI Endpoint is enabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt index 0a6f297ba04..378f5942dd0 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt @@ -520,6 +520,13 @@ Security: feature is enabled. Possible values are: non-strict, strict. Default: disabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt index a3698dc3363..acd42eab629 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt @@ -930,6 +930,13 @@ OpenAPI configuration: available at '/openapi/ui'. Default: false. Available only when OpenAPI Endpoint is enabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt index daf08d1f3d3..085aaf5ed6b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt @@ -568,6 +568,13 @@ Security: feature is enabled. Possible values are: non-strict, strict. Default: disabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt index f3a4e79f9ba..66de910e89b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt @@ -931,6 +931,13 @@ OpenAPI configuration: available at '/openapi/ui'. Default: false. Available only when OpenAPI Endpoint is enabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt index b5af01ed61c..bfa4d102fab 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt @@ -486,6 +486,13 @@ Truststore: List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or directories containing those files that will be used as a system truststore. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt index 1c48cd79348..c6eebbd3805 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt @@ -819,6 +819,13 @@ Truststore: List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or directories containing those files that will be used as a system truststore. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt index deed44552e8..103b2655b6b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt @@ -567,6 +567,13 @@ Security: feature is enabled. Possible values are: non-strict, strict. Default: disabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt index ec5b36e69d0..e605d601e02 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt @@ -930,6 +930,13 @@ OpenAPI configuration: available at '/openapi/ui'. Default: false. Available only when OpenAPI Endpoint is enabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt index 53dcb706b98..fc17b6e4744 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt @@ -565,6 +565,13 @@ Security: feature is enabled. Possible values are: non-strict, strict. Default: disabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt index a64e0c56ebc..42713437f12 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt @@ -928,6 +928,13 @@ OpenAPI configuration: available at '/openapi/ui'. Default: false. Available only when OpenAPI Endpoint is enabled. +Server configuration: + +--server-async-bootstrap + If true, endpoints are opened while the bootstrap runs in the background. If + false, endpoints are opened after bootstrap completes, ensuring the server + is ready to handle requests. Default: true. + Bootstrap Admin: --bootstrap-admin-client-id diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java index 6ea4b0c39db..41f47430c8e 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/junit5/extension/KeycloakDistributionDecorator.java @@ -43,6 +43,7 @@ public class KeycloakDistributionDecorator implements KeycloakDistribution { List args = new ArrayList<>(rawArgs); args.addAll(List.of(config.defaultOptions())); setEnvVar("KC_SHUTDOWN_DELAY", "0s"); + setEnvVar("KC_SERVER_ASYNC_BOOTSTRAP", "false"); return delegate.run(new ServerOptions(storageConfig, databaseConfig, args)); } diff --git a/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtectorFactory.java b/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtectorFactory.java index f592e050e1b..37155daa739 100755 --- a/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtectorFactory.java +++ b/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtectorFactory.java @@ -18,6 +18,7 @@ package org.keycloak.services.managers; import java.util.List; +import java.util.Optional; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; @@ -52,7 +53,7 @@ public class DefaultBruteForceProtectorFactory implements BruteForceProtectorFac @Override public void close() { - protector.shutdown(); + Optional.ofNullable(protector).ifPresent(DefaultBruteForceProtector::shutdown); } @Override diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 89289479fc7..98e4c509f6a 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -17,6 +17,9 @@ package org.keycloak.services.resources; import java.io.File; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import jakarta.ws.rs.core.Application; @@ -40,13 +43,15 @@ import org.jboss.logging.Logger; * @version $Revision: 1 $ * */ -public abstract class KeycloakApplication extends Application { +public abstract class KeycloakApplication extends Application { private static final String KC_TMPDIR = "kc.io.tmpdir"; private static final Logger logger = Logger.getLogger(KeycloakApplication.class); - private static KeycloakSessionFactory sessionFactory; + private static volatile KeycloakSessionFactory sessionFactory; + // Set to true when bootstrap is completed. It never changes back to false. + private static volatile boolean bootstrapCompleted = false; public KeycloakApplication() { try { @@ -81,10 +86,33 @@ public abstract class KeycloakApplication extends Application { protected void startup() { Profile.getInstance().logUnsupportedFeatures(); CryptoIntegration.init(KeycloakApplication.class.getClassLoader()); - KeycloakApplication.sessionFactory = createSessionFactory(); + var ksf = createSessionFactory(); + sessionFactory = ksf; - setTransactionTimeout(); - var exportImportManager = KeycloakModelUtils.runJobInTransactionWithResult(sessionFactory, session -> { + if (supportsAsyncInitialization()) { + final var executor = Executors.newSingleThreadExecutor(); + CompletableFuture.runAsync(() -> runBootstrap(ksf), executor) + .exceptionally(throwable -> { + exit(throwable); + return null; + }) + .thenRun(executor::shutdown); + return; + } + + runBootstrap(ksf); + } + + protected boolean supportsAsyncInitialization() { + return false; + } + + private void runBootstrap(KSF keycloakSessionFactory) { + var startTime = System.nanoTime(); + + initKeycloakSessionFactory(keycloakSessionFactory); + setTransactionTimeout(keycloakSessionFactory); + var exportImportManager = KeycloakModelUtils.runJobInTransactionWithResult(keycloakSessionFactory, session -> { DBLockManager dbLockManager = new DBLockManager(session); dbLockManager.checkForcedUnlock(); DBLockProvider dbLock = dbLockManager.getDBLock(); @@ -101,11 +129,15 @@ public abstract class KeycloakApplication extends Application { exportImportManager.runExport(); } - resetTransactionTimeout(); - sessionFactory.publish(new PostMigrationEvent(sessionFactory)); + resetTransactionTimeout(keycloakSessionFactory); + bootstrapCompleted = true; + keycloakSessionFactory.publish(new PostMigrationEvent(keycloakSessionFactory)); + + var duration = Duration.ofNanos(System.nanoTime() - startTime); + logger.infof("Bootstrap completed in %f seconds", (double) duration.toMillis() / 1000); } - protected int getTransactionTimeout(KeycloakSessionFactory sessionFactory) { + protected int getTransactionTimeout(KSF sessionFactory) { return Math.toIntExact(TimeUnit.MINUTES.toSeconds(5)); } @@ -144,24 +176,30 @@ public abstract class KeycloakApplication extends Application { protected abstract void initAndStart(); - protected abstract KeycloakSessionFactory createSessionFactory(); + protected abstract KSF createSessionFactory(); + + protected abstract void initKeycloakSessionFactory(KSF ksf); public static KeycloakSessionFactory getSessionFactory() { return sessionFactory; } - private void setTransactionTimeout() { + public static boolean isBootstrapCompleted() { + return bootstrapCompleted; + } + + private void setTransactionTimeout(KSF keycloakSessionFactory) { try { - var transactionTimeoutSeconds = getTransactionTimeout(sessionFactory); - KeycloakModelUtils.setTransactionLimit(sessionFactory, transactionTimeoutSeconds); + var transactionTimeoutSeconds = getTransactionTimeout(keycloakSessionFactory); + KeycloakModelUtils.setTransactionLimit(keycloakSessionFactory, transactionTimeoutSeconds); } catch (Exception e) { logger.debug("Failed to set the transaction timeout, using the default value"); } } - private void resetTransactionTimeout() { + private void resetTransactionTimeout(KSF keycloakSessionFactory) { try { - KeycloakModelUtils.setTransactionLimit(sessionFactory, 0); + KeycloakModelUtils.setTransactionLimit(keycloakSessionFactory, 0); } catch (Exception e) { logger.debug("Failed to reset the transaction timeout"); } diff --git a/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java b/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java index d1ff73b2e9a..836350ea5da 100644 --- a/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java +++ b/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java @@ -26,7 +26,6 @@ import org.keycloak.common.profile.PropertiesProfileConfigResolver; import org.keycloak.common.util.MultiSiteUtils; import org.keycloak.exportimport.ExportImportManager; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.services.error.KcUnrecognizedPropertyExceptionHandler; import org.keycloak.services.error.KeycloakErrorHandler; @@ -42,7 +41,7 @@ import org.keycloak.services.resources.WelcomeResource; import org.keycloak.services.resources.admin.AdminRoot; import org.keycloak.services.util.ObjectMapperResolver; -public class ResteasyKeycloakApplication extends KeycloakApplication { +public class ResteasyKeycloakApplication extends KeycloakApplication { protected Set singletons = new HashSet<>(); protected Set> classes = new HashSet<>(); @@ -91,10 +90,13 @@ public class ResteasyKeycloakApplication extends KeycloakApplication { } @Override - protected KeycloakSessionFactory createSessionFactory() { - ResteasyKeycloakSessionFactory factory = new ResteasyKeycloakSessionFactory(); - factory.init(); - return factory; + protected ResteasyKeycloakSessionFactory createSessionFactory() { + return new ResteasyKeycloakSessionFactory(); + } + + @Override + protected void initKeycloakSessionFactory(ResteasyKeycloakSessionFactory resteasyKeycloakSessionFactory) { + resteasyKeycloakSessionFactory.init(); } @Override diff --git a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java index f5257c83d82..4acc478038b 100644 --- a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java +++ b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java @@ -75,6 +75,7 @@ public class ClusteredKeycloakServer implements KeycloakServer { } catch (TimeoutException e) { throw new RuntimeException("Expected %d cluster members".formatted(numServers), e); } + ReadinessProbe.waitUntilReady(this::getManagementBaseUrl, numServers); } private void startContainersWithMixedImage(KeycloakServerConfigBuilder configBuilder, String[] imagePeServer, CountdownLatchLoggingConsumer clusterLatch) { diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java index 17ff0249a11..ded73032276 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java @@ -97,6 +97,7 @@ public class DistributionKeycloakServer implements KeycloakServer { OutputHandler outputHandler = startKeycloak(args); waitForStart(outputHandler); + ReadinessProbe.waitUntilReady(this); if (!Environment.isWindows()) { FileUtils.writeToFile(getPidFile(), ProcessUtils.getKeycloakPid(keycloakProcess)); diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java index b7c40a733c1..d063c7c9ee2 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java @@ -22,6 +22,7 @@ public class EmbeddedKeycloakServer implements KeycloakServer { } keycloak = builder.start(keycloakServerConfigBuilder.toArgs()); + ReadinessProbe.waitUntilReady(this); } @Override diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/ReadinessProbe.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/ReadinessProbe.java new file mode 100644 index 00000000000..49cdd843125 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/ReadinessProbe.java @@ -0,0 +1,96 @@ +package org.keycloak.testframework.server; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.IntFunction; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Polls the server's endpoint until it reports ready, supporting both HTTP and HTTPS connections. + */ +public final class ReadinessProbe { + + private static final long STARTUP_TIMEOUT_MILLIS = Duration.ofMinutes(5).toMillis(); + private static final int CONNECTION_TIMEOUT_MILLIS = Math.toIntExact(Duration.ofSeconds(5).toMillis()); + private static final long POLL_INTERVAL_MILLIS = Duration.ofMillis(500).toMillis(); + + private ReadinessProbe() { + } + + public static void waitUntilReady(KeycloakServer server) { + waitUntilReady(index -> server.getBaseUrl(), 1); + } + + public static void waitUntilReady(IntFunction baseUrlFunction, int clusterSize) { + var deadline = System.currentTimeMillis() + STARTUP_TIMEOUT_MILLIS; + var sslContext = createTrustAllSslContext(); + for (int i = 0; i < clusterSize; i++) { + // can't use /health/ready has it is not enabled in most tests + var url = baseUrlFunction.apply(i) + "/realms/master"; + waitUntilReady(url, sslContext, deadline); + } + } + + private static void waitUntilReady(String url, SSLContext sslContext, long deadline) { + while (System.currentTimeMillis() < deadline) { + try { + HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection(); + if (connection instanceof HttpsURLConnection https) { + https.setSSLSocketFactory(sslContext.getSocketFactory()); + https.setHostnameVerifier((hostname, session) -> true); + } + connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLIS); + connection.setReadTimeout(CONNECTION_TIMEOUT_MILLIS); + connection.setRequestMethod("GET"); + + try { + if (connection.getResponseCode() == 200) { + return; + } + } finally { + connection.disconnect(); + } + } catch (Exception e) { + // server not yet available, retry + } + + try { + //noinspection BusyWait + Thread.sleep(POLL_INTERVAL_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for server readiness", e); + } + } + throw new IllegalStateException("Server did not become ready within " + TimeUnit.MILLISECONDS.toSeconds(STARTUP_TIMEOUT_MILLIS) + " seconds: " + url); + } + + private static SSLContext createTrustAllSslContext() { + try { + TrustManager[] trustAll = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + } + }; + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, trustAll, null); + return ctx; + } catch (Exception e) { + throw new RuntimeException("Failed to create trust-all SSLContext", e); + } + } +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java index 3643beef6d6..99eda233020 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java @@ -30,6 +30,7 @@ public class RemoteKeycloakServer implements KeycloakServer { } waitForStartup(); } + ReadinessProbe.waitUntilReady(this); } @Override