Asynchronous server initialization

Closes #47187

Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Signed-off-by: Pedro Ruivo <pruivo@users.noreply.github.com>
Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: Steven Hawkins <shawkins@redhat.com>
This commit is contained in:
Pedro Ruivo
2026-03-19 20:23:46 +00:00
committed by GitHub
parent 37c9fd4de0
commit c93b6a7e6c
40 changed files with 542 additions and 48 deletions
@@ -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.
+9 -1
View File
@@ -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
@@ -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"/>.
@@ -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);
@@ -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<Boolean> 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();
}
@@ -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<BuildTimeConditionBuildItem> removeBeans, CombinedIndexBuildItem index) {
ClassInfo disabledBean = index.getIndex().getClassByName(DotName.createSimple(BoostrapReadyHealthCheck.class.getName()));
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean.asClass(), false));
}
@BuildStep
void disableMdcContextFilter(BuildProducer<BuildTimeConditionBuildItem> removeBeans, CombinedIndexBuildItem index) {
if (!Configuration.isTrue(LoggingOptions.LOG_MDC_ENABLED)) {
@@ -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
}
}
}
@@ -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<PropertyMapperGrouping> getPropertyMapperGroupings() {
@@ -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<? extends PropertyMapper<?>> getPropertyMappers() {
return List.of(
fromOption(ServerOptions.SERVER_ASYNC_BOOTSTRAP)
.paramLabel("enabled")
.build()
);
}
}
@@ -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<QuarkusKeycloakSessionFactory> {
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();
}
@@ -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();
}
}
@@ -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<HealthCheckResponse> 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<HealthCheckResponse> ready() {
return Uni.createFrom().item(UP);
}
private static HealthCheckResponseBuilder builder() {
return HealthCheckResponse.named("Keycloak Initialized");
}
}
@@ -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<Instant> failingSince = new AtomicReference<>();
private final InfinispanConnectionProviderFactory factory;
public KeycloakClusterReadyHealthCheck(InfinispanConnectionProviderFactory factory) {
this.factory = factory;
}
@Override
public Uni<HealthCheckResponse> 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 {
@@ -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;
}
}
@@ -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);
}
@@ -269,4 +269,11 @@ Truststore:
in a container environment. Default: true.
--truststore-paths <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.
directories containing those files that will be used as a system truststore.
Server configuration:
--server-async-bootstrap <enabled>
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.
@@ -271,4 +271,11 @@ Truststore:
in a container environment. Default: true.
--truststore-paths <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.
directories containing those files that will be used as a system truststore.
Server configuration:
--server-async-bootstrap <enabled>
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.
@@ -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 <enabled>
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 <client id>
@@ -569,6 +569,13 @@ OpenAPI configuration:
available at '/openapi/ui'. Default: false. Available only when OpenAPI
Endpoint is enabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -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 <enabled>
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 <client id>
@@ -564,6 +564,13 @@ OpenAPI configuration:
available at '/openapi/ui'. Default: false. Available only when OpenAPI
Endpoint is enabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -520,6 +520,13 @@ Security:
feature is enabled. Possible values are: non-strict, strict. Default:
disabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -930,6 +930,13 @@ OpenAPI configuration:
available at '/openapi/ui'. Default: false. Available only when OpenAPI
Endpoint is enabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -568,6 +568,13 @@ Security:
feature is enabled. Possible values are: non-strict, strict. Default:
disabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -931,6 +931,13 @@ OpenAPI configuration:
available at '/openapi/ui'. Default: false. Available only when OpenAPI
Endpoint is enabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -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 <enabled>
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 <client id>
@@ -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 <enabled>
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 <client id>
@@ -567,6 +567,13 @@ Security:
feature is enabled. Possible values are: non-strict, strict. Default:
disabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -930,6 +930,13 @@ OpenAPI configuration:
available at '/openapi/ui'. Default: false. Available only when OpenAPI
Endpoint is enabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -565,6 +565,13 @@ Security:
feature is enabled. Possible values are: non-strict, strict. Default:
disabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -928,6 +928,13 @@ OpenAPI configuration:
available at '/openapi/ui'. Default: false. Available only when OpenAPI
Endpoint is enabled.
Server configuration:
--server-async-bootstrap <enabled>
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 <client id>
@@ -43,6 +43,7 @@ public class KeycloakDistributionDecorator implements KeycloakDistribution {
List<String> 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));
}
@@ -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
@@ -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<KSF extends KeycloakSessionFactory> 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");
}
@@ -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<ResteasyKeycloakSessionFactory> {
protected Set<Object> singletons = new HashSet<>();
protected Set<Class<?>> 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
@@ -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) {
@@ -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));
@@ -22,6 +22,7 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
}
keycloak = builder.start(keycloakServerConfigBuilder.toArgs());
ReadinessProbe.waitUntilReady(this);
}
@Override
@@ -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<String> 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);
}
}
}
@@ -30,6 +30,7 @@ public class RemoteKeycloakServer implements KeycloakServer {
}
waitForStartup();
}
ReadinessProbe.waitUntilReady(this);
}
@Override