Upgrade to Quarkus 3.7.1 (#26736)
Closes #26701 Closes #23854 Signed-off-by: Václav Muzikář <vmuzikar@redhat.com>
This commit is contained in:
parent
00742a62dd
commit
8833b9d2ac
12 changed files with 48 additions and 308 deletions
4
pom.xml
4
pom.xml
|
@ -45,8 +45,8 @@
|
||||||
<jboss.snapshots.repo.id>jboss-snapshots-repository</jboss.snapshots.repo.id>
|
<jboss.snapshots.repo.id>jboss-snapshots-repository</jboss.snapshots.repo.id>
|
||||||
<jboss.snapshots.repo.url>https://s01.oss.sonatype.org/content/repositories/snapshots/</jboss.snapshots.repo.url>
|
<jboss.snapshots.repo.url>https://s01.oss.sonatype.org/content/repositories/snapshots/</jboss.snapshots.repo.url>
|
||||||
|
|
||||||
<quarkus.version>3.7.0.CR1</quarkus.version>
|
<quarkus.version>3.7.1</quarkus.version>
|
||||||
<quarkus.build.version>3.7.0.CR1</quarkus.build.version>
|
<quarkus.build.version>3.7.1</quarkus.build.version>
|
||||||
|
|
||||||
<project.build-time>${timestamp}</project.build-time>
|
<project.build-time>${timestamp}</project.build-time>
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,4 @@ public class HealthOptions {
|
||||||
.buildTime(true)
|
.buildTime(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public static final Option<Boolean> HEALTH_CLASSIC_PROBES_ENABLED = new OptionBuilder<>("health-classic-probes-enabled", Boolean.class)
|
|
||||||
.category(OptionCategory.HEALTH)
|
|
||||||
.description("If enabled, use the original Quarkus blocking handlers for '/health/ready' and '/health/live' endpoints.")
|
|
||||||
.defaultValue(Boolean.FALSE)
|
|
||||||
.buildTime(true)
|
|
||||||
.hidden()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,12 @@
|
||||||
|
|
||||||
package org.keycloak.quarkus.deployment;
|
package org.keycloak.quarkus.deployment;
|
||||||
|
|
||||||
|
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
|
||||||
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
|
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
|
||||||
import io.quarkus.agroal.spi.JdbcDriverBuildItem;
|
import io.quarkus.agroal.spi.JdbcDriverBuildItem;
|
||||||
|
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
|
||||||
import io.quarkus.arc.deployment.BuildTimeConditionBuildItem;
|
import io.quarkus.arc.deployment.BuildTimeConditionBuildItem;
|
||||||
|
import io.quarkus.arc.processor.AnnotationsTransformer;
|
||||||
import io.quarkus.bootstrap.logging.InitialConfigurator;
|
import io.quarkus.bootstrap.logging.InitialConfigurator;
|
||||||
import io.quarkus.datasource.deployment.spi.DevServicesDatasourceResultBuildItem;
|
import io.quarkus.datasource.deployment.spi.DevServicesDatasourceResultBuildItem;
|
||||||
import io.quarkus.deployment.IsDevelopment;
|
import io.quarkus.deployment.IsDevelopment;
|
||||||
|
@ -45,6 +48,7 @@ import io.quarkus.runtime.configuration.ProfileManager;
|
||||||
import io.quarkus.vertx.http.deployment.RouteBuildItem;
|
import io.quarkus.vertx.http.deployment.RouteBuildItem;
|
||||||
import io.quarkus.resteasy.reactive.spi.IgnoreStackMixingBuildItem;
|
import io.quarkus.resteasy.reactive.spi.IgnoreStackMixingBuildItem;
|
||||||
import io.smallrye.config.ConfigValue;
|
import io.smallrye.config.ConfigValue;
|
||||||
|
import org.eclipse.microprofile.health.Readiness;
|
||||||
import org.hibernate.cfg.AvailableSettings;
|
import org.hibernate.cfg.AvailableSettings;
|
||||||
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
|
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
|
||||||
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
|
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
|
||||||
|
@ -82,8 +86,6 @@ import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.provider.ProviderManager;
|
import org.keycloak.provider.ProviderManager;
|
||||||
import org.keycloak.provider.Spi;
|
import org.keycloak.provider.Spi;
|
||||||
import org.keycloak.quarkus.runtime.integration.health.ReactiveLivenessHandler;
|
|
||||||
import org.keycloak.quarkus.runtime.integration.health.ReactiveReadinessHandler;
|
|
||||||
import org.keycloak.quarkus.runtime.Environment;
|
import org.keycloak.quarkus.runtime.Environment;
|
||||||
import org.keycloak.quarkus.runtime.KeycloakRecorder;
|
import org.keycloak.quarkus.runtime.KeycloakRecorder;
|
||||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||||
|
@ -95,7 +97,6 @@ import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||||
import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakHandlerChainCustomizer;
|
import org.keycloak.quarkus.runtime.integration.resteasy.KeycloakHandlerChainCustomizer;
|
||||||
import org.keycloak.quarkus.runtime.integration.web.NotFoundHandler;
|
import org.keycloak.quarkus.runtime.integration.web.NotFoundHandler;
|
||||||
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyAsyncHealthCheck;
|
|
||||||
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
|
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
|
||||||
import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory;
|
import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory;
|
||||||
import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFactory;
|
import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFactory;
|
||||||
|
@ -616,37 +617,27 @@ class KeycloakProcessor {
|
||||||
|
|
||||||
if (healthDisabled) {
|
if (healthDisabled) {
|
||||||
routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT.concat("/*")).handler(new NotFoundHandler()).build());
|
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());
|
|
||||||
routes.produce(RouteBuildItem.builder().route(DEFAULT_HEALTH_ENDPOINT.concat("/ready")).handler(new ReactiveReadinessHandler()).build());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean metricsDisabled = !isMetricsEnabled();
|
boolean metricsDisabled = !isMetricsEnabled();
|
||||||
|
|
||||||
if (healthDisabled || metricsDisabled) {
|
if (healthDisabled || metricsDisabled) {
|
||||||
// disables the single check we provide which depends on metrics enabled
|
// disables the single check we provide which depends on metrics enabled
|
||||||
ClassInfo disabledBean1 = index.getIndex()
|
ClassInfo disabledBean = index.getIndex()
|
||||||
.getClassByName(DotName.createSimple(KeycloakReadyHealthCheck.class.getName()));
|
.getClassByName(DotName.createSimple(KeycloakReadyHealthCheck.class.getName()));
|
||||||
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean1.asClass(), false));
|
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean.asClass(), false));
|
||||||
ClassInfo disabledBean2 = index.getIndex()
|
|
||||||
.getClassByName(DotName.createSimple(KeycloakReadyAsyncHealthCheck.class.getName()));
|
|
||||||
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean2.asClass(), false));
|
|
||||||
} else {
|
|
||||||
if (isHealthClassicProbesEnabled()) {
|
|
||||||
// disable new async check
|
|
||||||
ClassInfo disabledBean2 = index.getIndex()
|
|
||||||
.getClassByName(DotName.createSimple(KeycloakReadyAsyncHealthCheck.class.getName()));
|
|
||||||
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean2.asClass(), false));
|
|
||||||
} else {
|
|
||||||
// disable old classic check
|
|
||||||
ClassInfo disabledBean1 = index.getIndex()
|
|
||||||
.getClassByName(DotName.createSimple(KeycloakReadyHealthCheck.class.getName()));
|
|
||||||
removeBeans.produce(new BuildTimeConditionBuildItem(disabledBean1.asClass(), false));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We can't use quarkus.datasource.health.enabled=false as that would remove the DataSourceHealthCheck from CDI and
|
||||||
|
// it can't be instantiated via constructor as it now includes some field injection points. So we just make it a regular
|
||||||
|
// bean without the @Readiness annotation so it won't be used as a health check on it's own.
|
||||||
|
@BuildStep
|
||||||
|
AnnotationsTransformerBuildItem disableDefaultDataSourceHealthCheck() {
|
||||||
|
return new AnnotationsTransformerBuildItem(AnnotationsTransformer.appliedToClass()
|
||||||
|
.whenClass(c -> c.name().equals(DotName.createSimple(DataSourceHealthCheck.class)))
|
||||||
|
.thenTransform(t -> t.remove(
|
||||||
|
a -> a.name().equals(DotName.createSimple(Readiness.class)))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@BuildStep
|
@BuildStep
|
||||||
|
@ -883,10 +874,6 @@ class KeycloakProcessor {
|
||||||
return Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("health-enabled")).orElse(false);
|
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<JdbcDataSourceBuildItem> jdbcDataSources) {
|
static JdbcDataSourceBuildItem getDefaultDataSource(List<JdbcDataSourceBuildItem> jdbcDataSources) {
|
||||||
for (JdbcDataSourceBuildItem jdbcDataSource : jdbcDataSources) {
|
for (JdbcDataSourceBuildItem jdbcDataSource : jdbcDataSources) {
|
||||||
if (jdbcDataSource.isDefault()) {
|
if (jdbcDataSource.isDefault()) {
|
||||||
|
|
|
@ -14,8 +14,6 @@ final class HealthPropertyMappers {
|
||||||
fromOption(HealthOptions.HEALTH_ENABLED)
|
fromOption(HealthOptions.HEALTH_ENABLED)
|
||||||
.to("quarkus.health.extensions.enabled")
|
.to("quarkus.health.extensions.enabled")
|
||||||
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
|
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
|
||||||
.build(),
|
|
||||||
fromOption(HealthOptions.HEALTH_CLASSIC_PROBES_ENABLED)
|
|
||||||
.build()
|
.build()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* This is a workaround until <a href="https://github.com/quarkusio/quarkus/pull/35100">quarkusio/quarkus#35100</a> is available
|
|
||||||
* in a regular Quarkus version. Then these classes can be removed.
|
|
||||||
*
|
|
||||||
* @author Alexander Schwartz
|
|
||||||
*/
|
|
||||||
public abstract class ReactiveHealthHandler implements Handler<RoutingContext> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(RoutingContext context) {
|
|
||||||
Uni<SmallRyeHealth> health = getHealth();
|
|
||||||
health.subscribe().with(smallRyeHealth -> {
|
|
||||||
new SmallRyeLivenessHandler() {
|
|
||||||
@Override
|
|
||||||
protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) {
|
|
||||||
return smallRyeHealth;
|
|
||||||
}
|
|
||||||
}.handle(context);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract Uni<SmallRyeHealth> getHealth();
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* 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<SmallRyeHealth> getHealth() {
|
|
||||||
SmallRyeHealthReporter healthReporter = Arc.container().instance(SmallRyeHealthReporter.class).get();
|
|
||||||
return healthReporter.getLivenessAsync();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 ReactiveReadinessHandler extends ReactiveHealthHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Uni<SmallRyeHealth> getHealth() {
|
|
||||||
SmallRyeHealthReporter healthReporter = Arc.container().instance(SmallRyeHealthReporter.class).get();
|
|
||||||
return healthReporter.getReadinessAsync();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2020 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.services.health;
|
|
||||||
|
|
||||||
import io.agroal.api.AgroalDataSource;
|
|
||||||
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
|
|
||||||
import io.quarkus.smallrye.health.runtime.QuarkusAsyncHealthCheckFactory;
|
|
||||||
import io.smallrye.health.api.AsyncHealthCheck;
|
|
||||||
import io.smallrye.mutiny.Uni;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
|
||||||
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
|
||||||
import org.eclipse.microprofile.health.Readiness;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keycloak Healthcheck Readiness Probe.
|
|
||||||
* <p>
|
|
||||||
* Performs a hybrid between the passive and the active mode. If there are no healthy connections in the pool,
|
|
||||||
* it invokes the standard <code>DataSourceHealthCheck</code> that creates a new connection and checks if it's valid.
|
|
||||||
* <p>
|
|
||||||
* While the check for healthy connections is non-blocking, the standard check is blocking, so it needs to be wrapped.
|
|
||||||
* <p>
|
|
||||||
* When NON_BLOCKING_PROBES is no longer behind a feature flag but the only option, it will replace the
|
|
||||||
* {@link KeycloakReadyHealthCheck}.
|
|
||||||
*
|
|
||||||
* @see <a href="https://github.com/keycloak/keycloak-community/pull/55">Healthcheck API Design</a>
|
|
||||||
*/
|
|
||||||
@Readiness
|
|
||||||
@ApplicationScoped
|
|
||||||
public class KeycloakReadyAsyncHealthCheck implements AsyncHealthCheck {
|
|
||||||
|
|
||||||
/** As the DataSourceHealthCheck doesn't exist as an application scoped bean,
|
|
||||||
* create our own instance here which exposes the <code>init()</code> call for the delegate. */
|
|
||||||
MyDataSourceHealthCheck delegate;
|
|
||||||
|
|
||||||
private static class MyDataSourceHealthCheck extends DataSourceHealthCheck {
|
|
||||||
@Override
|
|
||||||
public void init() {
|
|
||||||
super.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
protected void init() {
|
|
||||||
delegate = new MyDataSourceHealthCheck();
|
|
||||||
delegate.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Date formatter, the same as used by Quarkus. This enables users to quickly compare the date printed
|
|
||||||
* by the probe with the logs.
|
|
||||||
*/
|
|
||||||
static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS").withZone(ZoneId.systemDefault());
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AgroalDataSource agroalDataSource;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
QuarkusAsyncHealthCheckFactory healthCheckFactory;
|
|
||||||
|
|
||||||
AtomicReference<Instant> failingSince = new AtomicReference<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uni<HealthCheckResponse> call() {
|
|
||||||
HealthCheckResponseBuilder builder = HealthCheckResponse.named("Keycloak database connections async health check").up();
|
|
||||||
long activeCount = agroalDataSource.getMetrics().activeCount();
|
|
||||||
long invalidCount = agroalDataSource.getMetrics().invalidCount();
|
|
||||||
if (activeCount < 1 || invalidCount > 0) {
|
|
||||||
return healthCheckFactory.callSync(() -> {
|
|
||||||
HealthCheckResponse activeCheckResult = delegate.call();
|
|
||||||
if (activeCheckResult.getStatus() == HealthCheckResponse.Status.DOWN) {
|
|
||||||
builder.down();
|
|
||||||
Instant failingTime = failingSince.updateAndGet(this::createInstanceIfNeeded);
|
|
||||||
builder.withData("Failing since", DATE_FORMATTER.format(failingTime));
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
failingSince.set(null);
|
|
||||||
return healthCheckFactory.callAsync(() -> Uni.createFrom().item(builder.build()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Instant createInstanceIfNeeded(Instant instant) {
|
|
||||||
if (instant == null) {
|
|
||||||
return Instant.now();
|
|
||||||
}
|
|
||||||
return instant;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,12 +18,15 @@ package org.keycloak.quarkus.runtime.services.health;
|
||||||
|
|
||||||
import io.agroal.api.AgroalDataSource;
|
import io.agroal.api.AgroalDataSource;
|
||||||
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
|
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
|
||||||
|
import io.quarkus.smallrye.health.runtime.QuarkusAsyncHealthCheckFactory;
|
||||||
|
import io.smallrye.health.api.AsyncHealthCheck;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||||
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
||||||
import org.eclipse.microprofile.health.Readiness;
|
import org.eclipse.microprofile.health.Readiness;
|
||||||
|
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
|
||||||
import jakarta.inject.Inject;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
@ -31,15 +34,17 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keycloak Healthcheck Readiness Probe.
|
* Keycloak Healthcheck Readiness Probe.
|
||||||
*
|
* <p>
|
||||||
* Performs a hybrid between the passive and the active mode. If there are no healthy connections in the pool,
|
* Performs a hybrid between the passive and the active mode. If there are no healthy connections in the pool,
|
||||||
* it invokes the standard <code>DataSourceHealthCheck</code> that creates a new connection and checks if its valid.
|
* it invokes the standard <code>DataSourceHealthCheck</code> that creates a new connection and checks if it's valid.
|
||||||
|
* <p>
|
||||||
|
* While the check for healthy connections is non-blocking, the standard check is blocking, so it needs to be wrapped.
|
||||||
*
|
*
|
||||||
* @see <a href="https://github.com/keycloak/keycloak-community/pull/55">Healthcheck API Design</a>
|
* @see <a href="https://github.com/keycloak/keycloak-community/pull/55">Healthcheck API Design</a>
|
||||||
*/
|
*/
|
||||||
@Readiness
|
@Readiness
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class KeycloakReadyHealthCheck extends DataSourceHealthCheck {
|
public class KeycloakReadyHealthCheck implements AsyncHealthCheck {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date formatter, the same as used by Quarkus. This enables users to quickly compare the date printed
|
* Date formatter, the same as used by Quarkus. This enables users to quickly compare the date printed
|
||||||
|
@ -50,24 +55,33 @@ public class KeycloakReadyHealthCheck extends DataSourceHealthCheck {
|
||||||
@Inject
|
@Inject
|
||||||
AgroalDataSource agroalDataSource;
|
AgroalDataSource agroalDataSource;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
QuarkusAsyncHealthCheckFactory healthCheckFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
DataSourceHealthCheck dataSourceHealthCheck;
|
||||||
|
|
||||||
AtomicReference<Instant> failingSince = new AtomicReference<>();
|
AtomicReference<Instant> failingSince = new AtomicReference<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HealthCheckResponse call() {
|
public Uni<HealthCheckResponse> call() {
|
||||||
HealthCheckResponseBuilder builder = HealthCheckResponse.named("Keycloak database connections health check").up();
|
HealthCheckResponseBuilder builder = HealthCheckResponse.named("Keycloak database connections async health check").up();
|
||||||
long activeCount = agroalDataSource.getMetrics().activeCount();
|
long activeCount = agroalDataSource.getMetrics().activeCount();
|
||||||
long invalidCount = agroalDataSource.getMetrics().invalidCount();
|
long invalidCount = agroalDataSource.getMetrics().invalidCount();
|
||||||
if (activeCount < 1 || invalidCount > 0) {
|
if (activeCount < 1 || invalidCount > 0) {
|
||||||
HealthCheckResponse activeCheckResult = super.call();
|
return healthCheckFactory.callSync(() -> {
|
||||||
|
HealthCheckResponse activeCheckResult = dataSourceHealthCheck.call();
|
||||||
if (activeCheckResult.getStatus() == HealthCheckResponse.Status.DOWN) {
|
if (activeCheckResult.getStatus() == HealthCheckResponse.Status.DOWN) {
|
||||||
builder.down();
|
builder.down();
|
||||||
Instant failingTime = failingSince.updateAndGet(this::createInstanceIfNeeded);
|
Instant failingTime = failingSince.updateAndGet(this::createInstanceIfNeeded);
|
||||||
builder.withData("Failing since", DATE_FORMATTER.format(failingTime));
|
builder.withData("Failing since", DATE_FORMATTER.format(failingTime));
|
||||||
}
|
}
|
||||||
|
return builder.build();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
failingSince.set(null);
|
failingSince.set(null);
|
||||||
|
return healthCheckFactory.callAsync(() -> Uni.createFrom().item(builder.build()));
|
||||||
}
|
}
|
||||||
return builder.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Instant createInstanceIfNeeded(Instant instant) {
|
Instant createInstanceIfNeeded(Instant instant) {
|
||||||
|
|
|
@ -7,7 +7,6 @@ quarkus.banner.enabled=false
|
||||||
|
|
||||||
# Disable health checks from extensions, since we provide our own (default is true)
|
# Disable health checks from extensions, since we provide our own (default is true)
|
||||||
quarkus.health.extensions.enabled=false
|
quarkus.health.extensions.enabled=false
|
||||||
quarkus.datasource.health.enabled=false
|
|
||||||
|
|
||||||
# Disable http metrics binder as URL parameters are only shown with placeholders for '/resource' URLs, but not
|
# Disable http metrics binder as URL parameters are only shown with placeholders for '/resource' URLs, but not
|
||||||
# for '/admin' and '/realms'. Neither the IDs of entities nor the realm name should be part of the metric names
|
# for '/admin' and '/realms'. Neither the IDs of entities nor the realm name should be part of the metric names
|
||||||
|
|
|
@ -80,19 +80,6 @@ public class HealthDistTest {
|
||||||
.statusCode(404);
|
.statusCode(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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));
|
|
||||||
when().get("/lb-check").then()
|
|
||||||
.statusCode(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testUsingRelativePath(KeycloakDistribution distribution) {
|
void testUsingRelativePath(KeycloakDistribution distribution) {
|
||||||
for (String relativePath : List.of("/auth", "/auth/", "auth")) {
|
for (String relativePath : List.of("/auth", "/auth/", "auth")) {
|
||||||
|
|
|
@ -266,6 +266,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>mysql</groupId>
|
<groupId>mysql</groupId>
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
<version>${mysql-jdbc.version}</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
Loading…
Reference in a new issue