From 89e3c5194356f75176da480aab362736ea89ce39 Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Tue, 12 May 2026 13:14:20 -0400 Subject: [PATCH] fix: refining startup, and adding a log on async start error closes: #48438 Signed-off-by: Steve Hawkins --- .../quarkus/runtime/KeycloakMain.java | 16 ++++++- .../jaxrs/QuarkusKeycloakApplication.java | 38 ++++++++-------- .../realm/TestRealmResourceFactory.java | 7 ++- .../it/cli/dist/StartCommandDistTest.java | 21 +++++++++ .../it/utils/RawKeycloakDistribution.java | 2 +- .../resources/KeycloakApplication.java | 43 ++++--------------- .../resteasy/ResteasyKeycloakApplication.java | 23 +++------- 7 files changed, 75 insertions(+), 75 deletions(-) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java index e9f4f6100a58..c61ad766d6c4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java @@ -18,8 +18,10 @@ package org.keycloak.quarkus.runtime; import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.concurrent.ForkJoinPool; +import java.util.function.Consumer; import jakarta.enterprise.context.ApplicationScoped; @@ -57,6 +59,7 @@ public class KeycloakMain implements QuarkusApplication { private static AbstractNonServerCommand COMMAND; + private static Consumer ERROR_HANDLER; static { InfinispanUtils.configureVirtualThreads(); @@ -132,6 +135,9 @@ private static void ensureForkJoinPoolThreadFactoryHasBeenSetToQuarkus() { public static void start(Picocli picocli, AbstractNonServerCommand command, ExecutionExceptionHandler errorHandler) { COMMAND = command; // it would be nice to not do this statically - start quarkus with an instance of KeycloakMain, rather than a class for example + ERROR_HANDLER = cause -> errorHandler.error(picocli.getErrWriter(), + String.format("Failed to start server in (%s) mode", getKeycloakModeFromProfile(org.keycloak.common.util.Environment.getProfile())), + cause.getCause()); try { Quarkus.run(KeycloakMain.class, (exitCode, cause) -> { if (cause != null) { @@ -145,6 +151,9 @@ public static void start(Picocli picocli, AbstractNonServerCommand command, Exec errorHandler.error(picocli.getErrWriter(), String.format("Unexpected error when starting the server in (%s) mode", getKeycloakModeFromProfile(org.keycloak.common.util.Environment.getProfile())), cause.getCause()); + } finally { + ERROR_HANDLER = null; + COMMAND = null; } picocli.exit(CommandLine.ExitCode.SOFTWARE); } @@ -169,5 +178,10 @@ public int run(String... args) throws Exception { return ApplicationLifecycleManager.getExitCode(); } - + + public static void asyncExit(int exitCode, Throwable t) { + Optional.ofNullable(ERROR_HANDLER).ifPresent(h -> h.accept(t)); + Quarkus.asyncExit(exitCode); + } + } 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 6184abe1f9e5..52eacf8477a2 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 @@ -17,6 +17,8 @@ package org.keycloak.quarkus.runtime.integration.jaxrs; +import java.util.concurrent.CompletableFuture; + import jakarta.enterprise.event.Observes; import jakarta.ws.rs.ApplicationPath; @@ -25,6 +27,7 @@ import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.KeycloakMain; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor; @@ -37,11 +40,11 @@ import org.keycloak.utils.StringUtil; import io.quarkus.arc.Arc; -import io.quarkus.runtime.Quarkus; import io.quarkus.runtime.ShutdownDelayInitiatedEvent; import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.StartupEvent; import io.smallrye.common.annotation.Blocking; +import org.eclipse.microprofile.context.ManagedExecutor; import org.jboss.logging.Logger; import static org.keycloak.common.util.Environment.isDevMode; @@ -65,13 +68,20 @@ protected String getDataDir() { }); } - @Override - protected void exit(Throwable cause) { - Quarkus.asyncExit(1); - } - void onStartupEvent(@Observes StartupEvent event) { - startup(); + var asyncBootstrap = Configuration.getOptionalKcValue(ServerOptions.SERVER_ASYNC_BOOTSTRAP) + .map(Boolean::parseBoolean) + .orElse(Boolean.TRUE); + // skip async bootstrap in dev and non-server mode + if (isDevMode() || isNonServerMode() || hasEarlyExitLaunchMode() || !asyncBootstrap) { + startup(); + } else { + ManagedExecutor executor = Arc.container().instance(ManagedExecutor.class).get(); + CompletableFuture.runAsync(this::startup, executor).exceptionally(cause -> { + KeycloakMain.asyncExit(1, cause); + return null; + }); + } } void onShutdownEvent(@Observes ShutdownEvent event) { @@ -87,11 +97,6 @@ public DefaultKeycloakSessionFactory createSessionFactory() { return Arc.container().instance(QuarkusKeycloakSessionFactory.class).get(); } - @Override - protected void initAndStart() { - // no need - is handled by Quarkus logic and onStartupEvent - } - @Override protected void createTemporaryAdmin(KeycloakSession session) { var adminUsername = getOption(BootstrapAdminOptions.USERNAME.getKey(), KEYCLOAK_ADMIN_ENV_VAR); @@ -113,15 +118,6 @@ protected void createTemporaryAdmin(KeycloakSession session) { } } - @Override - 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() && !hasEarlyExitLaunchMode() && asyncBootstrap; - } - @Override protected int getTransactionTimeout(DefaultKeycloakSessionFactory sessionFactory) { return ((QuarkusJpaConnectionProviderFactory) sessionFactory.getProviderFactory(JpaConnectionProvider.class)).getMigrationTransactionTimeout(); diff --git a/quarkus/tests/integration/src/test-providers/java/org/keycloak/it/resource/realm/TestRealmResourceFactory.java b/quarkus/tests/integration/src/test-providers/java/org/keycloak/it/resource/realm/TestRealmResourceFactory.java index 51d8335fe051..10e97cd2bbf8 100644 --- a/quarkus/tests/integration/src/test-providers/java/org/keycloak/it/resource/realm/TestRealmResourceFactory.java +++ b/quarkus/tests/integration/src/test-providers/java/org/keycloak/it/resource/realm/TestRealmResourceFactory.java @@ -28,6 +28,7 @@ */ public class TestRealmResourceFactory implements RealmResourceProviderFactory { public static final String ID = "test-resources"; + private boolean fail; @Override public RealmResourceProvider create(KeycloakSession session) { @@ -36,12 +37,14 @@ public RealmResourceProvider create(KeycloakSession session) { @Override public void init(Config.Scope config) { - + fail = Boolean.parseBoolean(config.get("fail", null)); } @Override public void postInit(KeycloakSessionFactory factory) { - + if (fail) { + throw new RuntimeException("I've failed"); + } } @Override diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java index 9c44b191fa56..4a579ea73338 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java @@ -17,16 +17,22 @@ package org.keycloak.it.cli.dist; +import java.util.concurrent.TimeUnit; + import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DryRun; import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.junit5.extension.TestProvider; import org.keycloak.it.junit5.extension.WithEnvVars; +import org.keycloak.it.resource.realm.TestRealmResourceTestProvider; import org.keycloak.it.utils.KeycloakDistribution; import org.keycloak.it.utils.RawDistributionLifecycleManager; +import org.keycloak.it.utils.RawKeycloakDistribution; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.main.Launch; +import org.awaitility.Awaitility; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Tag; @@ -279,4 +285,19 @@ void testRuntimeValuesAreNotCaptured(KeycloakDistribution dist) { cliResult.assertNoBuild(); cliResult.assertStarted(); } + + @RawDistOnly(reason = "Containers are immutable") + @Test + @TestProvider(TestRealmResourceTestProvider.class) + void testAsyncBootstrapFails(KeycloakDistribution dist) { + RawKeycloakDistribution rawDist = dist.unwrap(RawKeycloakDistribution.class); + dist.setManualStop(true); + CLIResult result = dist.run("start", "--server-async-bootstrap=true", "--hostname-strict=false", "--db=dev-file", "--http-enabled=true", "--spi-realm-restapi-extension--test-resources--fail=true"); + Awaitility.await().atMost(2, TimeUnit.MINUTES).until(() -> !rawDist.isRunning()); + dist.stop(); + result.assertMessage("Failed to start server"); + result.assertMessage("I've failed"); + assertEquals(1, dist.getExitCode()); + } + } diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java index 7a3701e226c4..cbca23f07d20 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/RawKeycloakDistribution.java @@ -477,7 +477,7 @@ public X509Certificate[] getAcceptedIssuers() { return socketFactory; } - private boolean isRunning() { + public boolean isRunning() { return keycloak != null && keycloak.isAlive(); } 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 35bd515146eb..38063fde8535 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -19,8 +19,6 @@ import java.io.File; import java.time.Duration; import java.time.Instant; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import jakarta.ws.rs.core.Application; @@ -54,16 +52,6 @@ public abstract class KeycloakApplication extends Application { private static volatile DefaultKeycloakSessionFactory sessionFactory; - public KeycloakApplication() { - try { - initTmpDirectory(); - logger.debugv("Application: {0}", this.getClass().getName()); - initAndStart(); - } catch (Throwable t) { - exit(t); - } - } - public static String getTmpDirectory() { return System.getProperty(KC_TMPDIR, System.getProperty("java.io.tmpdir")); } @@ -80,34 +68,19 @@ protected void initTmpDirectory() { System.setProperty(KC_TMPDIR, tmpDir.getAbsolutePath()); } - protected abstract void exit(Throwable t); - protected abstract String getDataDir(); - protected void startup() { + // synchronized to prevent shutdown while running bootstrapping + protected synchronized void startup() { + logger.debugv("Application: {0}", this.getClass().getName()); + initTmpDirectory(); Profile.getInstance().logUnsupportedFeatures(); CryptoIntegration.init(KeycloakApplication.class.getClassLoader()); KeycloakApplication.sessionFactory = createSessionFactory(); - - if (supportsAsyncInitialization()) { - final var executor = Executors.newSingleThreadExecutor(); - CompletableFuture.runAsync(() -> runBootstrap(KeycloakApplication.sessionFactory), executor) - .exceptionally(throwable -> { - exit(throwable); - return null; - }) - .thenRun(executor::shutdown); - return; - } - runBootstrap(KeycloakApplication.sessionFactory); } - protected boolean supportsAsyncInitialization() { - return false; - } - - private synchronized void runBootstrap(DefaultKeycloakSessionFactory keycloakSessionFactory) { + private void runBootstrap(DefaultKeycloakSessionFactory keycloakSessionFactory) { var startTime = System.nanoTime(); keycloakSessionFactory.init(); @@ -189,10 +162,12 @@ protected ExportImportManager bootstrap(KeycloakSession session) { protected abstract void createTemporaryAdmin(KeycloakSession session); - protected abstract void initAndStart(); - protected abstract DefaultKeycloakSessionFactory createSessionFactory(); + /** + * WARNING: This method is for use by test logic. Will return null if there is no current KeycloakApplication, or if the + * startup has not yet reached the point of setting this value. + */ public static DefaultKeycloakSessionFactory getSessionFactory() { return sessionFactory; } 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 af6c0da56c45..436b27020742 100644 --- a/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java +++ b/services/src/test/java/org/keycloak/services/resteasy/ResteasyKeycloakApplication.java @@ -68,6 +68,13 @@ public ResteasyKeycloakApplication() { // an endpoint for the load balancer to gather information whether this site should receive requests or not. classes.add(LoadBalancerResource.class); } + Config.init(new JsonConfigProviderFactory().create() + .orElseThrow(() -> new RuntimeException("Failed to load Keycloak configuration"))); + Profile.configure( + new PropertiesProfileConfigResolver(System.getProperties()), + new PropertiesFileProfileConfigResolver() + ); + startup(); } @Override @@ -75,11 +82,6 @@ protected String getDataDir() { return System.getProperty("project.build.directory"); } - @Override - protected void exit(Throwable cause) { - throw new RuntimeException(cause); - } - @Override public Set> getClasses() { return classes; @@ -100,17 +102,6 @@ protected void createTemporaryAdmin(KeycloakSession session) { // do nothing } - @Override - protected void initAndStart() { - Config.init(new JsonConfigProviderFactory().create() - .orElseThrow(() -> new RuntimeException("Failed to load Keycloak configuration"))); - Profile.configure( - new PropertiesProfileConfigResolver(System.getProperties()), - new PropertiesFileProfileConfigResolver() - ); - startup(); - } - @Override protected ExportImportManager bootstrap(KeycloakSession session) { // created a nested transaction as this triggers DefaultJpaConnectionProviderFactory.lazyInit