Skip to content

Commit

Permalink
Redis Health Indicator for Spring Actuator
Browse files Browse the repository at this point in the history
  • Loading branch information
phgbecker committed Jan 29, 2025
1 parent 94cd17a commit a1725fc
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 2 deletions.
5 changes: 4 additions & 1 deletion docker/server/config/config-redis-os.properties
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ conductor.elasticsearch.clusterHealthColor=green

# Additional modules for metrics collection exposed to Prometheus (optional)
conductor.metrics-prometheus.enabled=true
management.endpoints.web.exposure.include=prometheus
management.endpoints.web.exposure.include=health,prometheus

# Redis health indicator
management.health.redis.enabled=true

# Load sample kitchen sink workflow
loadSample=true
Expand Down
5 changes: 4 additions & 1 deletion docker/server/config/config-redis.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ conductor.elasticsearch.clusterHealthColor=yellow

# Additional modules for metrics collection exposed to Prometheus (optional)
conductor.metrics-prometheus.enabled=true
management.endpoints.web.exposure.include=prometheus
management.endpoints.web.exposure.include=health,prometheus

# Redis health indicator
management.health.redis.enabled=true

# Load sample kitchen sink workflow
loadSample=true
1 change: 1 addition & 0 deletions redis-lock/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dependencies {

implementation "org.apache.commons:commons-lang3"
implementation "org.redisson:redisson:${revRedisson}"
implementation 'org.springframework.boot:spring-boot-starter-actuator'

testImplementation "org.testcontainers:testcontainers:${revTestContainer}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2025 Conductor Authors.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 com.netflix.conductor.redislock.config;

import org.redisson.api.RedissonClient;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.redisson.api.redisnode.RedisNodes.*;

@Component
@ConditionalOnProperty(name = "management.health.redis.enabled", havingValue = "true")
public class RedisHealthIndicator implements HealthIndicator {
private final RedissonClient redisClient;
private final RedisLockProperties redisProperties;

public RedisHealthIndicator(RedissonClient redisClient, RedisLockProperties redisProperties) {
this.redisClient = redisClient;
this.redisProperties = redisProperties;
}

@Override
public Health health() {
return isHealth() ? Health.up().build() : Health.down().build();
}

private boolean isHealth() {
switch (redisProperties.getServerType()) {
case SINGLE -> {
return redisClient.getRedisNodes(SINGLE).pingAll(5, SECONDS);
}

case CLUSTER -> {
return redisClient.getRedisNodes(CLUSTER).pingAll(5, SECONDS);
}

case SENTINEL -> {
return redisClient.getRedisNodes(SENTINEL_MASTER_SLAVE).pingAll(5, SECONDS);
}

default -> {
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2025 Conductor Authors.
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 com.netflix.conductor.redislock.config;

import java.util.concurrent.TimeUnit;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.redisson.api.RedissonClient;
import org.redisson.redisnode.RedissonClusterNodes;
import org.redisson.redisnode.RedissonSentinelMasterSlaveNodes;
import org.redisson.redisnode.RedissonSingleNode;
import org.springframework.boot.actuate.health.Health;
import org.springframework.test.context.junit4.SpringRunner;

import static com.netflix.conductor.redislock.config.RedisLockProperties.REDIS_SERVER_TYPE.*;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;

@RunWith(SpringRunner.class)
public class RedisHealthIndicatorTest {

@Mock private RedissonClient redissonClient;

@Test
public void shouldReturnAsHealthWhenServerTypeIsSingle() {
// Given a Redisson client
var redisProperties = new RedisLockProperties();
redisProperties.setServerType(SINGLE);

// And its mocks
var redisNode = Mockito.mock(RedissonSingleNode.class);
when(redissonClient.getRedisNodes(any())).thenReturn(redisNode);
when(redisNode.pingAll(anyLong(), any(TimeUnit.class))).thenReturn(true);

// When execute a health indicator
var actualHealth = new RedisHealthIndicator(redissonClient, redisProperties);

// Then should return as health
assertThat(actualHealth.health()).isEqualTo(Health.up().build());
}

@Test
public void shouldReturnAsHealthWhenServerTypeIsCluster() {
// Given a Redisson client
var redisProperties = new RedisLockProperties();
redisProperties.setServerType(CLUSTER);

// And its mocks
var redisNode = Mockito.mock(RedissonClusterNodes.class);
when(redissonClient.getRedisNodes(any())).thenReturn(redisNode);
when(redisNode.pingAll(anyLong(), any(TimeUnit.class))).thenReturn(true);

// When execute a health indicator
var actualHealth = new RedisHealthIndicator(redissonClient, redisProperties);

// Then should return as health
assertThat(actualHealth.health()).isEqualTo(Health.up().build());
}

@Test
public void shouldReturnAsHealthWhenServerTypeIsSentinel() {
// Given a Redisson client
var redisProperties = new RedisLockProperties();
redisProperties.setServerType(SENTINEL);

// And its mocks
var redisNode = Mockito.mock(RedissonSentinelMasterSlaveNodes.class);
when(redissonClient.getRedisNodes(any())).thenReturn(redisNode);
when(redisNode.pingAll(anyLong(), any(TimeUnit.class))).thenReturn(true);

// When execute a health indicator
var actualHealth = new RedisHealthIndicator(redissonClient, redisProperties);

// Then should return as health
assertThat(actualHealth.health()).isEqualTo(Health.up().build());
}

@Test
public void shouldReturnAsUnhealthyWhenAnyServerIsDown() {
// Given a Redisson client
var redisProperties = new RedisLockProperties();
redisProperties.setServerType(SINGLE);

// And its mocks
var redisNode = Mockito.mock(RedissonSingleNode.class);
when(redissonClient.getRedisNodes(any())).thenReturn(redisNode);
when(redisNode.pingAll(anyLong(), any(TimeUnit.class))).thenReturn(false);

// When execute a health indicator
var actualHealth = new RedisHealthIndicator(redissonClient, redisProperties);

// Then should return as unhealthy
assertThat(actualHealth.health()).isEqualTo(Health.down().build());
}
}

0 comments on commit a1725fc

Please sign in to comment.