8000 fix: refining startup, and adding a log on async start error by shawkins · Pull Request #48733 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -57,6 +59,7 @@
public class KeycloakMain implements QuarkusApplication {

private static AbstractNonServerCommand COMMAND;
private static Consumer<Throwable> ERROR_HANDLER;

static {
InfinispanUtils.configureVirtualThreads();
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ public X509Certificate[] getAcceptedIssuers() {
return socketFactory;
}

private boolean isRunning() {
public boolean isRunning() {
return keycloak != null && keycloak.isAlive();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}
Expand All @@ -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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the idea that I logged the exception here. I'm surprised to see it missing.

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();
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,20 @@ 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
protected String getDataDir() {
return System.getProperty("project.build.directory");
}

@Override
protected void exit(Throwable cause) {
throw new RuntimeException(cause);
}

@Override
public Set<Class<?>> getClasses() {
return classes;
Expand All @@ -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
Expand Down
Loading
0