Skip to content

Commit c86772c

Browse files
committed
Asynchronous server initialization
Closes keycloak#47187 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
1 parent 529c1a9 commit c86772c

File tree

39 files changed

+517
-47
lines changed

39 files changed

+517
-47
lines changed

docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ resource server. The only exception is the resource server itself, which can man
6969
Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
7070
It also lists significant changes to internal APIs.
7171

72+
=== Endpoints are opened while Keycloak is initializing
73+
74+
By default, {project_name} now opens its HTTP(S) and Management ports while initialization is still in progress.
75+
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.
76+
77+
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.
78+
7279
=== Dev Mode defaults to localhost
7380

7481
When running the server in dev mode on a platform other than Windows Subsystem For Linux, the `http-host` setting will default to localhost.

docs/guides/server/configuration-production.adoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ By default, there is no limit set.
5252
Set the option `http-max-queued-requests` to limit the number of queued requests to a given threshold matching your environment.
5353
Any request that exceeds this limit would return with an immediate `503 Server not Available` response.
5454

55+
== Server bootstrap behavior
56+
57+
By default, {project_name} opens its HTTP(S) and Management endpoints while initialization is still in progress in the background.
58+
This means the server starts accepting TCP connections before it is fully ready to handle requests.
59+
60+
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.
61+
62+
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:
63+
64+
<@kc.start parameters="--server-async-bootstrap=false"/>
65+
66+
With this setting, {project_name} opens its endpoints only after the bootstrap is complete and the server is ready to handle requests.
67+
5568
== Production grade database
5669
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"/>.
5770

quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public enum OptionCategory {
2525
EXPORT("Export", 130, ConfigSupportLevel.SUPPORTED),
2626
IMPORT("Import", 140, ConfigSupportLevel.SUPPORTED),
2727
OPENAPI("OpenAPI configuration", 150, ConfigSupportLevel.SUPPORTED),
28+
SERVER("Server configuration", 160, ConfigSupportLevel.SUPPORTED),
2829
BOOTSTRAP_ADMIN("Bootstrap Admin", 998, ConfigSupportLevel.SUPPORTED),
2930
GENERAL("General", 999, ConfigSupportLevel.SUPPORTED);
3031

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2026 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.keycloak.config;
19+
20+
public final class ServerOptions {
21+
22+
private ServerOptions() {}
23+
24+
public static final Option<Boolean> SERVER_ASYNC_BOOTSTRAP = new OptionBuilder<>("server-async-bootstrap", Boolean.class)
25+
.category(OptionCategory.SERVER)
26+
.defaultValue(Boolean.TRUE)
27+
.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.")
28+
.build();
29+
}

quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakHandlerChainCustomizer;
9797
import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakTracingCustomizer;
9898
import org.keycloak.quarkus.runtime.logging.ClearMappedDiagnosticContextFilter;
99+
import org.keycloak.quarkus.runtime.services.health.BoostrapReadyHealthCheck;
99100
import org.keycloak.quarkus.runtime.services.health.KeycloakClusterReadyHealthCheck;
100101
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
101102
import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory;
@@ -838,6 +839,7 @@ void disableHealthCheckBean(BuildProducer<BuildTimeConditionBuildItem> removeBea
838839
if (isHealthDisabled()) {
839840
disableReadyHealthCheck(removeBeans, index);
840841
disableClusterHealthCheck(removeBeans, index);
842+
disableBootstrapReadyHealthCheck(removeBeans, index);
841843
return;
842844
}
843845
if (isMetricsDisabled()) {
@@ -860,6 +862,11 @@ private static void disableReadyHealthCheck(BuildProducer<BuildTimeConditionBuil
860862
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean.asClass(), false));
861863
}
862864

865+
private static void disableBootstrapReadyHealthCheck(BuildProducer<BuildTimeConditionBuildItem> removeBeans, CombinedIndexBuildItem index) {
866+
ClassInfo disabledBean = index.getIndex().getClassByName(DotName.createSimple(BoostrapReadyHealthCheck.class.getName()));
867+
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean.asClass(), false));
868+
}
869+
863870
@BuildStep
864871
void disableMdcContextFilter(BuildProducer<BuildTimeConditionBuildItem> removeBeans, CombinedIndexBuildItem index) {
865872
if (!Configuration.isTrue(LoggingOptions.LOG_MDC_ENABLED)) {

quarkus/deployment/src/test/java/test/org/keycloak/quarkus/services/health/MetricsEnabledProfile.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ public Map<String, String> getConfigOverrides() {
3232
"kc.health-enabled","true",
3333
"kc.metrics-enabled", "true",
3434
"kc.cache", "local",
35+
"kc.server-async-bootstrap", "false",
3536
"quarkus.micrometer.export.prometheus.path", "/prom/metrics",
3637
"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
3738
}
3839

39-
}
40+
}

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ public final class PropertyMappers {
5454
new FeaturePropertyMappers(), new ImportPropertyMappers(), new ManagementPropertyMappers(),
5555
new MetricsPropertyMappers(), new OpenApiPropertyMappers(), new LoggingPropertyMappers(), new ProxyPropertyMappers(),
5656
new VaultPropertyMappers(), new TracingPropertyMappers(), new TransactionPropertyMappers(),
57-
new SecurityPropertyMappers(), new TruststorePropertyMappers(), new TelemetryPropertyMappers());
57+
new SecurityPropertyMappers(), new TruststorePropertyMappers(), new TelemetryPropertyMappers(),
58+
new ServerPropertyMappers());
5859
}
5960

6061
public static List<PropertyMapperGrouping> getPropertyMapperGroupings() {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2026 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.keycloak.quarkus.runtime.configuration.mappers;
19+
20+
import java.util.List;
21+
22+
import org.keycloak.config.ServerOptions;
23+
24+
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
25+
26+
public final class ServerPropertyMappers implements PropertyMapperGrouping {
27+
@Override
28+
public List<? extends PropertyMapper<?>> getPropertyMappers() {
29+
return List.of(
30+
fromOption(ServerOptions.SERVER_ASYNC_BOOTSTRAP)
31+
.paramLabel("enabled")
32+
.build()
33+
);
34+
}
35+
}

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import jakarta.ws.rs.ApplicationPath;
2222

2323
import org.keycloak.config.BootstrapAdminOptions;
24+
import org.keycloak.config.ServerOptions;
2425
import org.keycloak.connections.jpa.JpaConnectionProvider;
2526
import org.keycloak.models.KeycloakSession;
26-
import org.keycloak.models.KeycloakSessionFactory;
2727
import org.keycloak.quarkus.runtime.Environment;
2828
import org.keycloak.quarkus.runtime.configuration.Configuration;
2929
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
@@ -41,9 +41,12 @@
4141
import io.smallrye.common.annotation.Blocking;
4242
import org.jboss.logging.Logger;
4343

44+
import static org.keycloak.common.util.Environment.isDevMode;
45+
import static org.keycloak.common.util.Environment.isNonServerMode;
46+
4447
@ApplicationPath("/")
4548
@Blocking
46-
public class QuarkusKeycloakApplication extends KeycloakApplication {
49+
public class QuarkusKeycloakApplication extends KeycloakApplication<QuarkusKeycloakSessionFactory> {
4750

4851
private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN";
4952
private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD";
@@ -72,10 +75,13 @@ void onShutdownEvent(@Observes ShutdownEvent event) {
7275
}
7376

7477
@Override
75-
public KeycloakSessionFactory createSessionFactory() {
76-
QuarkusKeycloakSessionFactory instance = QuarkusKeycloakSessionFactory.getInstance();
77-
instance.init();
78-
return instance;
78+
public QuarkusKeycloakSessionFactory createSessionFactory() {
79+
return QuarkusKeycloakSessionFactory.getInstance();
80+
}
81+
82+
@Override
83+
protected void initKeycloakSessionFactory(QuarkusKeycloakSessionFactory quarkusKeycloakSessionFactory) {
84+
quarkusKeycloakSessionFactory.init();
7985
}
8086

8187
@Override
@@ -105,7 +111,16 @@ protected void createTemporaryAdmin(KeycloakSession session) {
105111
}
106112

107113
@Override
108-
protected int getTransactionTimeout(KeycloakSessionFactory sessionFactory) {
114+
protected boolean supportsAsyncInitialization() {
115+
var asyncBootstrap = Configuration.getOptionalKcValue(ServerOptions.SERVER_ASYNC_BOOTSTRAP)
116+
.map(Boolean::parseBoolean)
117+
.orElse(Boolean.TRUE);
118+
// skip async bootstrap in dev and non-server mode
119+
return !isDevMode() && !isNonServerMode() && asyncBootstrap;
120+
}
121+
122+
@Override
123+
protected int getTransactionTimeout(QuarkusKeycloakSessionFactory sessionFactory) {
109124
return ((QuarkusJpaConnectionProviderFactory) sessionFactory.getProviderFactory(JpaConnectionProvider.class)).getMigrationTransactionTimeout();
110125
}
111126

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.keycloak.quarkus.runtime.services;
2+
3+
import jakarta.enterprise.context.ApplicationScoped;
4+
import jakarta.ws.rs.container.ContainerRequestContext;
5+
import jakarta.ws.rs.core.Response;
6+
7+
import org.keycloak.services.resources.KeycloakApplication;
8+
9+
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
10+
11+
/**
12+
* Pre-matching request filter that returns a 503 Service Unavailable response while the server bootstrap is in progress.
13+
*/
14+
@ApplicationScoped
15+
public class BootstrapFilter {
16+
17+
private boolean ready;
18+
19+
@ServerRequestFilter(priority = 1, preMatching = true)
20+
public Response filter(ContainerRequestContext ignored) {
21+
if (ready) {
22+
// JVM branch prediction may optimize this code and saves on reading a static volatile field
23+
return null;
24+
}
25+
if (KeycloakApplication.isBootstrapCompleted()) {
26+
// Return null to continue the request chain normally
27+
ready = true;
28+
return null;
29+
}
30+
// Return 503 Service Unavailable
31+
return Response.status(Response.Status.SERVICE_UNAVAILABLE).build();
32+
33+
}
34+
}

0 commit comments

Comments
 (0)