fix: resetting failingSince on datasourceHealthCheck == UP (#35905)

closes: #35904

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2025-01-08 08:21:40 -05:00 committed by GitHub
parent 0783e2a058
commit 3db9689010
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 146 additions and 116 deletions

View File

@ -173,6 +173,11 @@
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>

View File

@ -16,42 +16,24 @@
*/
package test.org.keycloak.quarkus.services.health;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static io.restassured.RestAssured.given;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;
@QuarkusTest
@TestProfile(MetricsEnabledProfile.class)
class KeycloakMetricsConfigurationTest {
@BeforeAll
static void setUpAll() {
System.setProperty("KC_CACHE", "local");
}
@AfterAll
static void tearDownAll() {
System.clearProperty("KC_CACHE");
}
@BeforeEach
void setUp() {
RestAssured.port = 9001;
}
@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource("keycloak.conf", "META-INF/keycloak.conf"))
.overrideConfigKey("quarkus.micrometer.export.prometheus.path", "/prom/metrics")
.overrideConfigKey("quarkus.class-loading.removed-artifacts", "io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-oracle-deployment"); // config works a bit odd in unit tests, so this is to ensure we exclude Oracle to avoid ClassNotFound ex
@Test
void testMetrics() {
given().basePath("/")

View File

@ -16,41 +16,50 @@
*/
package test.org.keycloak.quarkus.services.health;
import io.agroal.api.AgroalDataSource;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import jakarta.inject.Inject;
import static io.restassured.RestAssured.given;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.keycloak.quarkus.runtime.services.health.KeycloakReadyHealthCheck;
import org.mockito.Mockito;
import io.agroal.api.AgroalDataSource;
import io.agroal.api.AgroalDataSourceMetrics;
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;
@QuarkusTest
@TestProfile(MetricsEnabledProfile.class)
public class KeycloakNegativeHealthCheckTest {
@Inject
@InjectMock
AgroalDataSource agroalDataSource;
@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource("keycloak.conf", "META-INF/keycloak.conf"))
.overrideConfigKey("quarkus.class-loading.removed-artifacts", "io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-oracle-deployment"); // config works a bit odd in unit tests, so this is to ensure we exclude Oracle to avoid ClassNotFound ex
@InjectMock
DataSourceHealthCheck dataSourceHealthCheck;
@Test
public void testReadinessDown() {
agroalDataSource.close();
AgroalDataSourceMetrics metrics = Mockito.mock(AgroalDataSourceMetrics.class);
Mockito.when(agroalDataSource.getMetrics()).thenReturn(metrics);
Mockito.when(dataSourceHealthCheck.call()).thenReturn(HealthCheckResponse.down("down"));
RestAssured.port = 9001;
System.setProperty("KC_CACHE", "local"); // avoid flaky port conflicts
given()
.when().get("/health/ready")
.then()
.statusCode(503)
.body(Matchers.containsString("DOWN"));
System.clearProperty("KC_CACHE");
.body(Matchers.allOf(Matchers.containsString("DOWN"), Matchers.containsString(KeycloakReadyHealthCheck.FAILING_SINCE)));
// now have an active connection, failing since should be cleared
Mockito.when(metrics.activeCount()).thenReturn(2L);
given()
.when().get("/health/ready")
.then()
.statusCode(200)
.body(Matchers.not(Matchers.containsString(KeycloakReadyHealthCheck.FAILING_SINCE)));
}
}

View File

@ -16,44 +16,26 @@
*/
package test.org.keycloak.quarkus.services.health;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;
@QuarkusTest
@TestProfile(MetricsEnabledProfileWithPath.class)
class KeycloakPathConfigurationTest {
@BeforeAll
static void setUpAll() {
System.setProperty("KC_CACHE", "local");
}
@AfterAll
static void tearDownAll() {
System.clearProperty("KC_CACHE");
}
@BeforeEach
void setUp() {
RestAssured.port = 9001;
}
@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource("keycloak.conf", "META-INF/keycloak.conf"))
.overrideConfigKey("kc.http-relative-path","/auth")
.overrideConfigKey("quarkus.micrometer.export.prometheus.path", "/prom/metrics")
.overrideConfigKey("quarkus.class-loading.removed-artifacts", "io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-oracle-deployment"); // config works a bit odd in unit tests, so this is to ensure we exclude Oracle to avoid ClassNotFound ex
@Test
void testMetrics() {
given().basePath("/")

View File

@ -16,42 +16,25 @@
*/
package test.org.keycloak.quarkus.services.health;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static io.restassured.RestAssured.given;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;
@QuarkusTest
@TestProfile(MetricsEnabledProfile.class)
public class KeycloakReadyHealthCheckTest {
@BeforeAll
static void setUpAll() {
System.setProperty("KC_CACHE", "local");
}
@AfterAll
static void tearDownAll() {
System.clearProperty("KC_CACHE");
}
@BeforeEach
void setUp() {
RestAssured.port = 9001;
}
@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource("keycloak.conf", "META-INF/keycloak.conf"))
.overrideConfigKey("quarkus.class-loading.removed-artifacts", "io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-oracle-deployment"); // config works a bit odd in unit tests, so this is to ensure we exclude Oracle to avoid ClassNotFound ex
@Test
public void testLivenessUp() {
given()

View File

@ -0,0 +1,39 @@
/*
* Copyright 2025 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 test.org.keycloak.quarkus.services.health;
import java.util.Map;
import io.quarkus.test.junit.QuarkusTestProfile;
public class MetricsEnabledProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of("kc.http-enabled", "true",
"kc.cluster", "local",
"kc.hostname-strict", "false",
"kc.db", "dev-mem",
"kc.health-enabled","true",
"kc.metrics-enabled", "true",
"kc.cache", "local",
"quarkus.micrometer.export.prometheus.path", "/prom/metrics",
"quarkus.class-loading.removed-artifacts", "io.quarkus:quarkus-jdbc-oracle,io.quarkus:quarkus-jdbc-oracle-deployment"); // config works a bit odd in unit tests, so this is to ensure we exclude Oracle to avoid ClassNotFound ex
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2025 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 test.org.keycloak.quarkus.services.health;
import java.util.HashMap;
import java.util.Map;
public class MetricsEnabledProfileWithPath extends MetricsEnabledProfile {
@Override
public Map<String, String> getConfigOverrides() {
HashMap<String, String> config = new HashMap<>();
config.put("kc.http-relative-path","/auth");
config.putAll(super.getConfigOverrides());
return config;
}
}

View File

@ -1,9 +0,0 @@
http-enabled=true
cluster=local
hostname-strict=false
hostname-strict-https=false
db=dev-mem
db-username = sa
db-password = keycloak
health-enabled=true
metrics-enabled=true

View File

@ -17,6 +17,7 @@
package org.keycloak.quarkus.runtime.services.health;
import io.agroal.api.AgroalDataSource;
import io.agroal.api.AgroalDataSourceMetrics;
import io.quarkus.agroal.runtime.health.DataSourceHealthCheck;
import io.quarkus.smallrye.health.runtime.QuarkusAsyncHealthCheckFactory;
import io.smallrye.health.api.AsyncHealthCheck;
@ -46,6 +47,8 @@ import java.util.concurrent.atomic.AtomicReference;
@ApplicationScoped
public class KeycloakReadyHealthCheck implements AsyncHealthCheck {
public static final String FAILING_SINCE = "Failing since";
/**
* Date formatter, the same as used by Quarkus. This enables users to quickly compare the date printed
* by the probe with the logs.
@ -66,15 +69,18 @@ public class KeycloakReadyHealthCheck implements AsyncHealthCheck {
@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();
AgroalDataSourceMetrics metrics = agroalDataSource.getMetrics();
long activeCount = metrics.activeCount();
long invalidCount = metrics.invalidCount();
if (activeCount < 1 || invalidCount > 0) {
return healthCheckFactory.callSync(() -> {
HealthCheckResponse activeCheckResult = dataSourceHealthCheck.call();
if (activeCheckResult.getStatus() == HealthCheckResponse.Status.DOWN) {
builder.down();
Instant failingTime = failingSince.updateAndGet(this::createInstanceIfNeeded);
builder.withData("Failing since", DATE_FORMATTER.format(failingTime));
builder.withData(FAILING_SINCE, DATE_FORMATTER.format(failingTime));
} else {
failingSince.set(null);
}
return builder.build();
});