diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/HealthOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/HealthOptions.java index 8c4fccda075..bb673d04a33 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/HealthOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/HealthOptions.java @@ -2,10 +2,19 @@ package org.keycloak.config; public class HealthOptions { - public static final Option HEALTH_ENABLED = new OptionBuilder<>("health-enabled", Boolean.class) + public static final Option HEALTH_ENABLED = new OptionBuilder<>("health-enabled", Boolean.class) .category(OptionCategory.HEALTH) .description("If the server should expose health check endpoints. If enabled, health checks are available at the '/health', '/health/ready' and '/health/live' endpoints.") .defaultValue(Boolean.FALSE) .buildTime(true) .build(); + + public static final Option HEALTH_CLASSIC_PROBES_ENABLED = new OptionBuilder<>("health-classic-probes-enabled", Boolean.class) + .category(OptionCategory.HEALTH) + .description("If enabled, use the original Quarkus blocking handler for '/health/ready'") + .defaultValue(Boolean.FALSE) + .buildTime(true) + .hidden() + .build(); + } diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index 3a3dcb854b5..5aed32aed65 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -75,7 +75,6 @@ import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionSpi; import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory; import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider; -import org.keycloak.models.map.storage.jpa.EventListenerIntegrator; import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory; import org.keycloak.policy.BlacklistPasswordPolicyProviderFactory; import org.keycloak.protocol.ProtocolMapperSpi; @@ -86,6 +85,7 @@ import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderManager; import org.keycloak.provider.Spi; +import org.keycloak.quarkus.runtime.integration.health.ReactiveLivenessHandler; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.KeycloakRecorder; import org.keycloak.quarkus.runtime.configuration.Configuration; @@ -147,7 +147,6 @@ import static org.keycloak.quarkus.runtime.Environment.getProviderFiles; import static org.keycloak.quarkus.runtime.KeycloakRecorder.DEFAULT_HEALTH_ENDPOINT; import static org.keycloak.quarkus.runtime.KeycloakRecorder.DEFAULT_METRICS_ENDPOINT; import static org.keycloak.quarkus.runtime.Providers.getProviderManager; -import static org.keycloak.quarkus.runtime.configuration.Configuration.getKcConfigValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames; @@ -633,6 +632,11 @@ class KeycloakProcessor { if (healthDisabled) { routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT.concat("/*")).handler(new NotFoundHandler()).build()); + } else { + // local solution until https://github.com/quarkusio/quarkus/issues/35099 is available in Quarkus + if (!isHealthClassicProbesEnabled()) { + routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT.concat("/live")).handler(new ReactiveLivenessHandler()).build()); + } } boolean metricsDisabled = !isMetricsEnabled(); @@ -876,6 +880,10 @@ class KeycloakProcessor { return Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("health-enabled")).orElse(false); } + private boolean isHealthClassicProbesEnabled() { + return Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("health-classic-probes-enabled")).orElse(false); + } + static JdbcDataSourceBuildItem getDefaultDataSource(List jdbcDataSources) { for (JdbcDataSourceBuildItem jdbcDataSource : jdbcDataSources) { if (jdbcDataSource.isDefault()) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HealthPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HealthPropertyMappers.java index a5b920037d0..e1a348501bb 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HealthPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HealthPropertyMappers.java @@ -14,6 +14,8 @@ final class HealthPropertyMappers { fromOption(HealthOptions.HEALTH_ENABLED) .to("quarkus.health.extensions.enabled") .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) + .build(), + fromOption(HealthOptions.HEALTH_CLASSIC_PROBES_ENABLED) .build() }; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/health/ReactiveHealthHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/health/ReactiveHealthHandler.java new file mode 100644 index 00000000000..7d3d98a1984 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/health/ReactiveHealthHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 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.integration.health; + +import io.quarkus.smallrye.health.runtime.SmallRyeLivenessHandler; +import io.smallrye.health.SmallRyeHealth; +import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * This adds the possibility to have a non-blocking health handler in Quarkus. + *

+ * Without a non-blocking health check, all liveness and readiness probes will enqueue in the worker thread pool. Under high load + * of if there is a lot of blocking IO happening (for example, during Keycloak cluster rebalancing), this leads to probes being queued. + * Queued probes would lead to timeouts unless the timeouts are configured to 10-20 seconds. Reactive probes avoid the enqueueing + * in the worker thread pool for all non-blocking probes, which will be the default for the (otherwise empty) liveness probe. + * For the readiness probe, this depends on the implementation of the specific readiness probes. + *

+ * This is a workaround until quarkusio/quarkus#35100 is available + * in a regular Quarkus version. Then these classes can be removed. + * + * @author Alexander Schwartz + */ +public abstract class ReactiveHealthHandler implements Handler { + + @Override + public void handle(RoutingContext context) { + Uni health = getHealth(); + health.subscribe().with(smallRyeHealth -> { + new SmallRyeLivenessHandler() { + @Override + protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return smallRyeHealth; + } + }.handle(context); + }); + } + + protected abstract Uni getHealth(); +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/health/ReactiveLivenessHandler.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/health/ReactiveLivenessHandler.java new file mode 100644 index 00000000000..c5e5c6db4d9 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/health/ReactiveLivenessHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 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.integration.health; + +import io.quarkus.arc.Arc; +import io.smallrye.health.SmallRyeHealth; +import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; + +/** + * @author Alexander Schwartz + */ +public class ReactiveLivenessHandler extends ReactiveHealthHandler { + + @Override + protected Uni getHealth() { + SmallRyeHealthReporter healthReporter = Arc.container().instance(SmallRyeHealthReporter.class).get(); + return healthReporter.getLivenessAsync(); + } +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java index f3d7421253d..e7f509f884d 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/HealthDistTest.java @@ -23,15 +23,11 @@ import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.utils.KeycloakDistribution; import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.equalTo; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; @DistributionTest(keepAlive =true) public class HealthDistTest { @@ -67,6 +63,28 @@ public class HealthDistTest { .statusCode(404); } + @Test + @Launch({ "start-dev", "--health-enabled=true", "--metrics-enabled=true" }) + void testNonBlockingProbes() { + when().get("/health/live").then() + .statusCode(200); + when().get("/health/ready").then() + .statusCode(200) + .body("checks[0].name", equalTo("Keycloak database connections health check")) + .body("checks.size()", equalTo(1)); + } + + @Test + @Launch({ "start-dev", "--health-enabled=true", "--metrics-enabled=true", "--health-classic-probes-enabled=true" }) + void testBlockingProbes() { + when().get("/health/live").then() + .statusCode(200); + when().get("/health/ready").then() + .statusCode(200) + .body("checks[0].name", equalTo("Keycloak database connections health check")) + .body("checks.size()", equalTo(1)); + } + @Test void testUsingRelativePath(KeycloakDistribution distribution) { for (String relativePath : List.of("/auth", "/auth/", "auth")) {