Skip to content

Commit 84a6da1

Browse files
committed
[PoC] Migration/Import in the background
Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
1 parent 0937fe6 commit 84a6da1

File tree

7 files changed

+131
-2
lines changed

7 files changed

+131
-2
lines changed

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/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
4949
private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD";
5050

5151
private static final Logger logger = Logger.getLogger(QuarkusKeycloakApplication.class);
52+
private static boolean bootstrapCompleted = false;
5253

5354
@Override
5455
protected String getDataDir() {
@@ -104,6 +105,21 @@ protected void createTemporaryAdmin(KeycloakSession session) {
104105
}
105106
}
106107

108+
@Override
109+
protected void onInitializationCompleted() {
110+
bootstrapCompleted = true;
111+
}
112+
113+
@Override
114+
protected boolean supportsAsyncInitialization() {
115+
// TODO support only for production environment, dev and non server probably won't need to do async init
116+
return !org.keycloak.common.util.Environment.isNonServerMode();
117+
}
118+
119+
public static boolean isBootstrapCompleted() {
120+
return bootstrapCompleted;
121+
}
122+
107123
@Override
108124
protected int getTransactionTimeout(KeycloakSessionFactory sessionFactory) {
109125
return ((QuarkusJpaConnectionProviderFactory) sessionFactory.getProviderFactory(JpaConnectionProvider.class)).getMigrationTransactionTimeout();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication;
8+
9+
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
10+
11+
@ApplicationScoped
12+
public class BootstrapFilter {
13+
14+
@ServerRequestFilter(priority = 1)
15+
public Response filter(ContainerRequestContext requestContext) {
16+
if (QuarkusKeycloakApplication.isBootstrapCompleted()) {
17+
// Return null to continue the request chain normally
18+
return null;
19+
}
20+
// Return 503 Service Unavailable
21+
return Response.status(Response.Status.SERVICE_UNAVAILABLE)
22+
.entity("Keycloak is initializing...")
23+
.build();
24+
25+
}
26+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.services.health;
19+
20+
import jakarta.enterprise.context.ApplicationScoped;
21+
22+
import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication;
23+
24+
import io.smallrye.health.api.AsyncHealthCheck;
25+
import io.smallrye.mutiny.Uni;
26+
import org.eclipse.microprofile.health.HealthCheckResponse;
27+
import org.eclipse.microprofile.health.Readiness;
28+
29+
@Readiness
30+
@ApplicationScoped
31+
public class BoostrapReadyHealthCheck implements AsyncHealthCheck {
32+
33+
@Override
34+
public Uni<HealthCheckResponse> call() {
35+
var builder = HealthCheckResponse.named("Keycloak Initialized");
36+
if (QuarkusKeycloakApplication.isBootstrapCompleted()) {
37+
builder.up();
38+
} else {
39+
builder.down();
40+
}
41+
return Uni.createFrom().item(builder.build());
42+
}
43+
}

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheck.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
public class KeycloakClusterReadyHealthCheck implements AsyncHealthCheck {
3535

36-
private final AtomicReference<Instant> failingSince = new AtomicReference<>();
36+
private static final AtomicReference<Instant> failingSince = new AtomicReference<>();
3737

3838
@Override
3939
public Uni<HealthCheckResponse> call() {

quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/services/health/KeycloakClusterReadyHealthCheckProducer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
2424
import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
2525
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
26+
import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication;
2627

2728
import io.smallrye.health.api.AsyncHealthCheck;
2829
import org.eclipse.microprofile.health.Readiness;
@@ -34,6 +35,9 @@ public class KeycloakClusterReadyHealthCheckProducer {
3435
@Readiness
3536
@Dependent
3637
public AsyncHealthCheck createHealthCheck() {
38+
if (!QuarkusKeycloakApplication.isBootstrapCompleted()) {
39+
return null;
40+
}
3741
var sessionFactory = QuarkusKeycloakSessionFactory.getInstance();
3842
InfinispanConnectionProviderFactory factory = (InfinispanConnectionProviderFactory) sessionFactory.getProviderFactory(InfinispanConnectionProvider.class);
3943
if (factory.isClusterHealthSupported()) {

services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.keycloak.services.resources;
1818

1919
import java.io.File;
20+
import java.util.concurrent.CompletableFuture;
2021
import java.util.concurrent.TimeUnit;
2122

2223
import jakarta.ws.rs.core.Application;
@@ -81,8 +82,37 @@ protected void initTmpDirectory() {
8182
protected void startup() {
8283
Profile.getInstance().logUnsupportedFeatures();
8384
CryptoIntegration.init(KeycloakApplication.class.getClassLoader());
84-
KeycloakApplication.sessionFactory = createSessionFactory();
85+
if (supportsAsyncInitialization()) {
86+
CompletableFuture.runAsync(this::initializeAndStart)
87+
.exceptionally(throwable -> {
88+
exit(throwable);
89+
return null;
90+
});
91+
return;
92+
}
93+
initializeAndStart();
94+
}
95+
96+
protected void onInitializationCompleted() {
97+
98+
}
99+
100+
protected boolean supportsAsyncInitialization() {
101+
return false;
102+
}
103+
104+
private void initializeAndStart() {
85105

106+
var slowStartDelay = System.getProperty("org.keycloak.start-delay");
107+
if (slowStartDelay != null) {
108+
try {
109+
Thread.sleep(TimeUnit.SECONDS.toMillis(Integer.parseInt(slowStartDelay)));
110+
} catch (InterruptedException e) {
111+
Thread.currentThread().interrupt();
112+
}
113+
}
114+
115+
KeycloakApplication.sessionFactory = createSessionFactory();
86116
setTransactionTimeout();
87117
var exportImportManager = KeycloakModelUtils.runJobInTransactionWithResult(sessionFactory, session -> {
88118
DBLockManager dbLockManager = new DBLockManager(session);
@@ -103,6 +133,9 @@ protected void startup() {
103133

104134
resetTransactionTimeout();
105135
sessionFactory.publish(new PostMigrationEvent(sessionFactory));
136+
onInitializationCompleted();
137+
// TODO log duration
138+
logger.info("Initialization completed");
106139
}
107140

108141
protected int getTransactionTimeout(KeycloakSessionFactory sessionFactory) {

0 commit comments

Comments
 (0)