Move code from spring-boot-actuator to spring-boot-data-redis

This commit is contained in:
Andy Wilkinson
2025-05-09 15:19:52 +01:00
committed by Phillip Webb
parent 631653cd1d
commit ee458e1909
12 changed files with 33 additions and 30 deletions

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.data.redis.actuate.health;
import java.util.Properties;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Health.Builder;
import org.springframework.data.redis.connection.ClusterInfo;
/**
* Shared class used by {@link RedisHealthIndicator} and
* {@link RedisReactiveHealthIndicator} to provide health details.
*
* @author Phillip Webb
*/
final class RedisHealth {
private RedisHealth() {
}
static Builder up(Health.Builder builder, Properties info) {
builder.withDetail("version", info.getProperty("redis_version"));
return builder.up();
}
static Builder fromClusterInfo(Health.Builder builder, ClusterInfo clusterInfo) {
builder.withDetail("cluster_size", clusterInfo.getClusterSize());
builder.withDetail("slots_up", clusterInfo.getSlotsOk());
builder.withDetail("slots_fail", clusterInfo.getSlotsFail());
if ("fail".equalsIgnoreCase(clusterInfo.getState())) {
return builder.down();
}
else {
return builder.up();
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.data.redis.actuate.health;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.util.Assert;
/**
* Simple implementation of a {@link HealthIndicator} returning status information for
* Redis data stores.
*
* @author Christian Dupuis
* @author Richard Santana
* @author Scott Frederick
* @since 4.0.0
*/
public class RedisHealthIndicator extends AbstractHealthIndicator {
private final RedisConnectionFactory redisConnectionFactory;
public RedisHealthIndicator(RedisConnectionFactory connectionFactory) {
super("Redis health check failed");
Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
this.redisConnectionFactory = connectionFactory;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
RedisConnection connection = RedisConnectionUtils.getConnection(this.redisConnectionFactory);
try {
doHealthCheck(builder, connection);
}
finally {
RedisConnectionUtils.releaseConnection(connection, this.redisConnectionFactory);
}
}
private void doHealthCheck(Health.Builder builder, RedisConnection connection) {
if (connection instanceof RedisClusterConnection clusterConnection) {
RedisHealth.fromClusterInfo(builder, clusterConnection.clusterGetClusterInfo());
}
else {
RedisHealth.up(builder, connection.serverCommands().info());
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.data.redis.actuate.health;
import java.util.Properties;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.data.redis.connection.ClusterInfo;
import org.springframework.data.redis.connection.ReactiveRedisClusterConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
/**
* A {@link ReactiveHealthIndicator} for Redis.
*
* @author Stephane Nicoll
* @author Mark Paluch
* @author Artsiom Yudovin
* @author Scott Frederick
* @since 4.0.0
*/
public class RedisReactiveHealthIndicator extends AbstractReactiveHealthIndicator {
private final ReactiveRedisConnectionFactory connectionFactory;
public RedisReactiveHealthIndicator(ReactiveRedisConnectionFactory connectionFactory) {
super("Redis health check failed");
this.connectionFactory = connectionFactory;
}
@Override
protected Mono<Health> doHealthCheck(Health.Builder builder) {
return getConnection().flatMap((connection) -> doHealthCheck(builder, connection));
}
private Mono<ReactiveRedisConnection> getConnection() {
return Mono.fromSupplier(this.connectionFactory::getReactiveConnection)
.subscribeOn(Schedulers.boundedElastic());
}
private Mono<Health> doHealthCheck(Health.Builder builder, ReactiveRedisConnection connection) {
return getHealth(builder, connection).onErrorResume((ex) -> Mono.just(builder.down(ex).build()))
.flatMap((health) -> connection.closeLater().thenReturn(health));
}
private Mono<Health> getHealth(Health.Builder builder, ReactiveRedisConnection connection) {
if (connection instanceof ReactiveRedisClusterConnection clusterConnection) {
return clusterConnection.clusterGetClusterInfo().map((info) -> fromClusterInfo(builder, info));
}
return connection.serverCommands().info("server").map((info) -> up(builder, info));
}
private Health up(Health.Builder builder, Properties info) {
return RedisHealth.up(builder, info).build();
}
private Health fromClusterInfo(Health.Builder builder, ClusterInfo clusterInfo) {
return RedisHealth.fromClusterInfo(builder, clusterInfo).build();
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.
*/
/**
* Redis health integration using Spring Data Redis.
*/
package org.springframework.boot.data.redis.actuate.health;

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.data.redis.actuate.health;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.ClusterInfo;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisServerCommands;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RedisHealthIndicator}.
*
* @author Christian Dupuis
* @author Richard Santana
* @author Stephane Nicoll
*/
class RedisHealthIndicatorTests {
@Test
void redisIsUp() {
Properties info = new Properties();
info.put("redis_version", "2.8.9");
RedisConnection redisConnection = mock(RedisConnection.class);
RedisServerCommands serverCommands = mock(RedisServerCommands.class);
given(redisConnection.serverCommands()).willReturn(serverCommands);
given(serverCommands.info()).willReturn(info);
RedisHealthIndicator healthIndicator = createHealthIndicator(redisConnection);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("version", "2.8.9");
}
@Test
void redisIsDown() {
RedisConnection redisConnection = mock(RedisConnection.class);
RedisServerCommands serverCommands = mock(RedisServerCommands.class);
given(redisConnection.serverCommands()).willReturn(serverCommands);
given(serverCommands.info()).willThrow(new RedisConnectionFailureException("Connection failed"));
RedisHealthIndicator healthIndicator = createHealthIndicator(redisConnection);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat((String) health.getDetails().get("error")).contains("Connection failed");
}
@Test
void healthWhenClusterStateIsAbsentShouldBeUp() {
RedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory(null);
RedisHealthIndicator healthIndicator = new RedisHealthIndicator(redisConnectionFactory);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("cluster_size", 4L);
assertThat(health.getDetails()).containsEntry("slots_up", 4L);
assertThat(health.getDetails()).containsEntry("slots_fail", 0L);
then(redisConnectionFactory).should(atLeastOnce()).getConnection();
}
@Test
void healthWhenClusterStateIsOkShouldBeUp() {
RedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("ok");
RedisHealthIndicator healthIndicator = new RedisHealthIndicator(redisConnectionFactory);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("cluster_size", 4L);
assertThat(health.getDetails()).containsEntry("slots_up", 4L);
assertThat(health.getDetails()).containsEntry("slots_fail", 0L);
then(redisConnectionFactory).should(atLeastOnce()).getConnection();
}
@Test
void healthWhenClusterStateIsFailShouldBeDown() {
RedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("fail");
RedisHealthIndicator healthIndicator = new RedisHealthIndicator(redisConnectionFactory);
Health health = healthIndicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).containsEntry("cluster_size", 4L);
assertThat(health.getDetails()).containsEntry("slots_up", 3L);
assertThat(health.getDetails()).containsEntry("slots_fail", 1L);
then(redisConnectionFactory).should(atLeastOnce()).getConnection();
}
private RedisHealthIndicator createHealthIndicator(RedisConnection redisConnection) {
RedisConnectionFactory redisConnectionFactory = mock(RedisConnectionFactory.class);
given(redisConnectionFactory.getConnection()).willReturn(redisConnection);
return new RedisHealthIndicator(redisConnectionFactory);
}
private RedisConnectionFactory createClusterConnectionFactory(String state) {
Properties clusterProperties = new Properties();
if (state != null) {
clusterProperties.setProperty("cluster_state", state);
}
clusterProperties.setProperty("cluster_size", "4");
boolean failure = "fail".equals(state);
clusterProperties.setProperty("cluster_slots_ok", failure ? "3" : "4");
clusterProperties.setProperty("cluster_slots_fail", failure ? "1" : "0");
List<RedisClusterNode> redisMasterNodes = Arrays.asList(new RedisClusterNode("127.0.0.1", 7001),
new RedisClusterNode("127.0.0.2", 7001));
RedisClusterConnection redisConnection = mock(RedisClusterConnection.class);
given(redisConnection.clusterGetNodes()).willReturn(redisMasterNodes);
given(redisConnection.clusterGetClusterInfo()).willReturn(new ClusterInfo(clusterProperties));
RedisConnectionFactory redisConnectionFactory = mock(RedisConnectionFactory.class);
given(redisConnectionFactory.getConnection()).willReturn(redisConnection);
return redisConnectionFactory;
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.data.redis.actuate.health;
import java.time.Duration;
import java.util.Properties;
import io.lettuce.core.RedisConnectionException;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.ClusterInfo;
import org.springframework.data.redis.connection.ReactiveRedisClusterConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.ReactiveServerCommands;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RedisReactiveHealthIndicator}.
*
* @author Stephane Nicoll
* @author Mark Paluch
* @author Nikolay Rybak
* @author Artsiom Yudovin
* @author Scott Frederick
*/
class RedisReactiveHealthIndicatorTests {
@Test
void redisIsUp() {
Properties info = new Properties();
info.put("redis_version", "2.8.9");
ReactiveRedisConnection redisConnection = mock(ReactiveRedisConnection.class);
given(redisConnection.closeLater()).willReturn(Mono.empty());
ReactiveServerCommands commands = mock(ReactiveServerCommands.class);
given(commands.info("server")).willReturn(Mono.just(info));
RedisReactiveHealthIndicator healthIndicator = createHealthIndicator(redisConnection, commands);
Mono<Health> health = healthIndicator.health();
StepVerifier.create(health).consumeNextWith((h) -> {
assertThat(h.getStatus()).isEqualTo(Status.UP);
assertThat(h.getDetails()).containsOnlyKeys("version");
assertThat(h.getDetails()).containsEntry("version", "2.8.9");
}).expectComplete().verify(Duration.ofSeconds(30));
then(redisConnection).should().closeLater();
}
@Test
void healthWhenClusterStateIsAbsentShouldBeUp() {
ReactiveRedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory(null);
RedisReactiveHealthIndicator healthIndicator = new RedisReactiveHealthIndicator(redisConnectionFactory);
Mono<Health> health = healthIndicator.health();
StepVerifier.create(health).consumeNextWith((h) -> {
assertThat(h.getStatus()).isEqualTo(Status.UP);
assertThat(h.getDetails()).containsEntry("cluster_size", 4L);
assertThat(h.getDetails()).containsEntry("slots_up", 4L);
assertThat(h.getDetails()).containsEntry("slots_fail", 0L);
}).expectComplete().verify(Duration.ofSeconds(30));
then(redisConnectionFactory.getReactiveConnection()).should().closeLater();
}
@Test
void healthWhenClusterStateIsOkShouldBeUp() {
ReactiveRedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("ok");
RedisReactiveHealthIndicator healthIndicator = new RedisReactiveHealthIndicator(redisConnectionFactory);
Mono<Health> health = healthIndicator.health();
StepVerifier.create(health).consumeNextWith((h) -> {
assertThat(h.getStatus()).isEqualTo(Status.UP);
assertThat(h.getDetails()).containsEntry("cluster_size", 4L);
assertThat(h.getDetails()).containsEntry("slots_up", 4L);
assertThat(h.getDetails()).containsEntry("slots_fail", 0L);
}).expectComplete().verify(Duration.ofSeconds(30));
}
@Test
void healthWhenClusterStateIsFailShouldBeDown() {
ReactiveRedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("fail");
RedisReactiveHealthIndicator healthIndicator = new RedisReactiveHealthIndicator(redisConnectionFactory);
Mono<Health> health = healthIndicator.health();
StepVerifier.create(health).consumeNextWith((h) -> {
assertThat(h.getStatus()).isEqualTo(Status.DOWN);
assertThat(h.getDetails()).containsEntry("slots_up", 3L);
assertThat(h.getDetails()).containsEntry("slots_fail", 1L);
}).expectComplete().verify(Duration.ofSeconds(30));
}
@Test
void redisCommandIsDown() {
ReactiveServerCommands commands = mock(ReactiveServerCommands.class);
given(commands.info("server")).willReturn(Mono.error(new RedisConnectionFailureException("Connection failed")));
ReactiveRedisConnection redisConnection = mock(ReactiveRedisConnection.class);
given(redisConnection.closeLater()).willReturn(Mono.empty());
RedisReactiveHealthIndicator healthIndicator = createHealthIndicator(redisConnection, commands);
Mono<Health> health = healthIndicator.health();
StepVerifier.create(health)
.consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN))
.expectComplete()
.verify(Duration.ofSeconds(30));
then(redisConnection).should().closeLater();
}
@Test
void redisConnectionIsDown() {
ReactiveRedisConnectionFactory redisConnectionFactory = mock(ReactiveRedisConnectionFactory.class);
given(redisConnectionFactory.getReactiveConnection())
.willThrow(new RedisConnectionException("Unable to connect to localhost:6379"));
RedisReactiveHealthIndicator healthIndicator = new RedisReactiveHealthIndicator(redisConnectionFactory);
Mono<Health> health = healthIndicator.health();
StepVerifier.create(health)
.consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN))
.expectComplete()
.verify(Duration.ofSeconds(30));
}
private RedisReactiveHealthIndicator createHealthIndicator(ReactiveRedisConnection redisConnection,
ReactiveServerCommands serverCommands) {
ReactiveRedisConnectionFactory redisConnectionFactory = mock(ReactiveRedisConnectionFactory.class);
given(redisConnectionFactory.getReactiveConnection()).willReturn(redisConnection);
given(redisConnection.serverCommands()).willReturn(serverCommands);
return new RedisReactiveHealthIndicator(redisConnectionFactory);
}
private ReactiveRedisConnectionFactory createClusterConnectionFactory(String state) {
Properties clusterProperties = new Properties();
if (state != null) {
clusterProperties.setProperty("cluster_state", state);
}
clusterProperties.setProperty("cluster_size", "4");
boolean failure = "fail".equals(state);
clusterProperties.setProperty("cluster_slots_ok", failure ? "3" : "4");
clusterProperties.setProperty("cluster_slots_fail", failure ? "1" : "0");
ReactiveRedisClusterConnection redisConnection = mock(ReactiveRedisClusterConnection.class);
given(redisConnection.closeLater()).willReturn(Mono.empty());
given(redisConnection.clusterGetClusterInfo()).willReturn(Mono.just(new ClusterInfo(clusterProperties)));
ReactiveRedisConnectionFactory redisConnectionFactory = mock(ReactiveRedisConnectionFactory.class);
given(redisConnectionFactory.getReactiveConnection()).willReturn(redisConnection);
return redisConnectionFactory;
}
}