Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/failover.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,18 +209,18 @@ The health check system operates independently of your application traffic, runn

#### Available Health Check Types

##### 1. EchoStrategy (Default)
##### 1. PingStrategy (Default)

The `EchoStrategy` is the default health check implementation that uses Redis's `ECHO` command to verify both connectivity and write capability.
The `PingStrategy` is the default health check implementation that uses Redis's `PING` command to verify both connectivity and write capability.

**Use Cases:**
- General-purpose health checking for most Redis deployments
- Verifying both read and write operations
- Simple connectivity validation

**How it works:**
- Sends `ECHO "HealthCheck"` command to the Redis server
- Expects exact response `"HealthCheck"` to consider the server healthy
- Sends `PING` command to the Redis server
- Expects exact response `"PONG"` to consider the server healthy
- Any exception or unexpected response marks the server as unhealthy

##### 2. LagAwareStrategy [PREVIEW] (Redis Enterprise)
Expand Down
24 changes: 12 additions & 12 deletions src/main/java/redis/clients/jedis/MultiDbConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisValidationException;
import redis.clients.jedis.mcf.ConnectionFailoverException;
import redis.clients.jedis.mcf.EchoStrategy;
import redis.clients.jedis.mcf.PingStrategy;
import redis.clients.jedis.mcf.HealthCheckStrategy;

/**
Expand Down Expand Up @@ -71,7 +71,7 @@
* </p>
* @see redis.clients.jedis.mcf.MultiDbConnectionProvider
* @see redis.clients.jedis.mcf.HealthCheckStrategy
* @see redis.clients.jedis.mcf.EchoStrategy
* @see redis.clients.jedis.mcf.PingStrategy
* @see redis.clients.jedis.mcf.LagAwareStrategy
* @since 7.0
*/
Expand All @@ -90,13 +90,13 @@ public final class MultiDbConfig {
* <strong>Common Implementations:</strong>
* </p>
* <ul>
* <li>{@link redis.clients.jedis.mcf.EchoStrategy#DEFAULT} - Uses Redis ECHO command for health
* <li>{@link redis.clients.jedis.mcf.PingStrategy#DEFAULT} - Uses Redis PING command for health
* checks</li>
* <li>Custom implementations for specific monitoring requirements</li>
* <li>Redis Enterprise implementations using REST API monitoring</li>
* </ul>
* @see redis.clients.jedis.mcf.HealthCheckStrategy
* @see redis.clients.jedis.mcf.EchoStrategy
* @see redis.clients.jedis.mcf.PingStrategy
* @see redis.clients.jedis.mcf.LagAwareStrategy
*/
public static interface StrategySupplier {
Expand Down Expand Up @@ -839,15 +839,15 @@ public static class DatabaseConfig {

/**
* Strategy supplier for creating health check instances for this database. Default is
* EchoStrategy.DEFAULT.
* PingStrategy.DEFAULT.
*/
private StrategySupplier healthCheckStrategySupplier;

/**
* Constructs a DatabaseConfig with basic endpoint and client configuration.
* <p>
* This constructor creates a database configuration with default settings: weight of 1.0f and
* EchoStrategy for health checks. Use the {@link Builder} for more advanced configuration
* PingStrategy for health checks. Use the {@link Builder} for more advanced configuration
* options.
* </p>
* @param endpoint the Redis endpoint (host and port)
Expand All @@ -863,7 +863,7 @@ public DatabaseConfig(Endpoint endpoint, JedisClientConfig clientConfig) {
* Constructs a DatabaseConfig with endpoint, client, and connection pool configuration.
* <p>
* This constructor allows specification of connection pool settings in addition to basic
* endpoint configuration. Default weight of 1.0f and EchoStrategy for health checks are used.
* endpoint configuration. Default weight of 1.0f and PingStrategy for health checks are used.
* </p>
* @param endpoint the Redis endpoint (host and port)
* @param clientConfig the Jedis client configuration
Expand Down Expand Up @@ -963,7 +963,7 @@ public StrategySupplier getHealthCheckStrategySupplier() {
* </p>
* <ul>
* <li><strong>Weight:</strong> 1.0f (standard priority)</li>
* <li><strong>Health Check:</strong> {@link redis.clients.jedis.mcf.EchoStrategy#DEFAULT}</li>
* <li><strong>Health Check:</strong> {@link redis.clients.jedis.mcf.PingStrategy#DEFAULT}</li>
* <li><strong>Connection Pool:</strong> null (uses default pooling)</li>
* </ul>
*/
Expand All @@ -980,8 +980,8 @@ public static class Builder {
/** Weight for database selection priority. Default: 1.0f */
private float weight = 1.0f;

/** Health check strategy supplier. Default: EchoStrategy.DEFAULT */
private StrategySupplier healthCheckStrategySupplier = EchoStrategy.DEFAULT;
/** Health check strategy supplier. Default: PingStrategy.DEFAULT */
private StrategySupplier healthCheckStrategySupplier = PingStrategy.DEFAULT;

/**
* Constructs a new Builder with required endpoint and client configuration.
Expand Down Expand Up @@ -1089,7 +1089,7 @@ public Builder healthCheckStrategy(HealthCheckStrategy healthCheckStrategy) {
* </ul>
* <p>
* When health checks are enabled (true) and no strategy supplier was previously set, the
* default {@link redis.clients.jedis.mcf.EchoStrategy#DEFAULT} will be used.
* default {@link redis.clients.jedis.mcf.PingStrategy#DEFAULT} will be used.
* </p>
* @param healthCheckEnabled true to enable health checks, false to disable
* @return this builder instance for method chaining
Expand All @@ -1098,7 +1098,7 @@ public Builder healthCheckEnabled(boolean healthCheckEnabled) {
if (!healthCheckEnabled) {
this.healthCheckStrategySupplier = null;
} else if (healthCheckStrategySupplier == null) {
this.healthCheckStrategySupplier = EchoStrategy.DEFAULT;
this.healthCheckStrategySupplier = PingStrategy.DEFAULT;
}
return this;
}
Expand Down
12 changes: 0 additions & 12 deletions src/main/java/redis/clients/jedis/UnifiedJedis.java
Original file line number Diff line number Diff line change
Expand Up @@ -194,18 +194,6 @@ public UnifiedJedis(ConnectionProvider provider, int maxAttempts, Duration maxTo
this(new RetryableCommandExecutor(provider, maxAttempts, maxTotalRetriesDuration), provider);
}

/**
* Constructor which supports multiple cluster/database endpoints each with their own isolated connection pool.
* <p>
* With this Constructor users can seamlessly failover to Disaster Recovery (DR), Backup, and Active-Active cluster(s)
* by using simple configuration which is passed through from Resilience4j - https://resilience4j.readme.io/docs
* <p>
*/
@Experimental
public UnifiedJedis(MultiDbConnectionProvider provider) {
this(new MultiDbCommandExecutor(provider), provider);
}

/**
* The constructor to use a custom {@link CommandExecutor}.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.MultiDbConfig.StrategySupplier;

public class EchoStrategy implements HealthCheckStrategy {
public class PingStrategy implements HealthCheckStrategy {
private static final int MAX_HEALTH_CHECK_POOL_SIZE = 2;

private final UnifiedJedis jedis;
private final HealthCheckStrategy.Config config;

public EchoStrategy(HostAndPort hostAndPort, JedisClientConfig jedisClientConfig) {
public PingStrategy(HostAndPort hostAndPort, JedisClientConfig jedisClientConfig) {
this(hostAndPort, jedisClientConfig, HealthCheckStrategy.Config.create());
}

public EchoStrategy(HostAndPort hostAndPort, JedisClientConfig jedisClientConfig,
public PingStrategy(HostAndPort hostAndPort, JedisClientConfig jedisClientConfig,
HealthCheckStrategy.Config config) {
GenericObjectPoolConfig<Connection> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(MAX_HEALTH_CHECK_POOL_SIZE);
Expand Down Expand Up @@ -55,15 +55,14 @@ public int getDelayInBetweenProbes() {

@Override
public HealthStatus doHealthCheck(Endpoint endpoint) {
return "HealthCheck".equals(jedis.echo("HealthCheck")) ? HealthStatus.HEALTHY
: HealthStatus.UNHEALTHY;
return "PONG".equals(jedis.ping()) ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY;
}

@Override
public void close() {
jedis.close();
}

public static final StrategySupplier DEFAULT = EchoStrategy::new;
public static final StrategySupplier DEFAULT = PingStrategy::new;

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import redis.clients.jedis.EndpointConfig;
import redis.clients.jedis.HostAndPorts;
import redis.clients.jedis.JedisClientConfig;
import redis.clients.jedis.MultiDbClient;
import redis.clients.jedis.MultiDbConfig;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
Expand Down Expand Up @@ -58,7 +59,7 @@ public class FailoverIntegrationTest {
private static String JEDIS1_ID = "";
private static String JEDIS2_ID = "";
private MultiDbConnectionProvider provider;
private UnifiedJedis failoverClient;
private MultiDbClient failoverClient;

@BeforeAll
public static void setupAdminClients() throws IOException {
Expand Down Expand Up @@ -110,7 +111,7 @@ public void setup() throws IOException {

// Create default provider and client for most tests
provider = createProvider();
failoverClient = new UnifiedJedis(provider);
failoverClient = MultiDbClient.builder().connectionProvider(provider).build();
}

@AfterEach
Expand Down Expand Up @@ -272,7 +273,7 @@ public void testCircuitBreakerCountsEachConnectionErrorSeparately() throws IOExc
.build();

MultiDbConnectionProvider provider = new MultiDbConnectionProvider(failoverConfig);
try (UnifiedJedis client = new UnifiedJedis(provider)) {
try (MultiDbClient client = MultiDbClient.builder().connectionProvider(provider).build()) {
// Verify initial connection to first endpoint
assertThat(getNodeId(client.info("server")), equalTo(JEDIS1_ID));

Expand Down Expand Up @@ -321,7 +322,8 @@ public void testInflightCommandsAreRetriedAfterFailover() throws Exception {
builder -> builder.retryOnFailover(true));

// Create a custom client with retryOnFailover enabled for this specific test
try (UnifiedJedis customClient = new UnifiedJedis(customProvider)) {
try (MultiDbClient customClient = MultiDbClient.builder().connectionProvider(customProvider)
.build()) {

assertThat(getNodeId(customClient.info("server")), equalTo(JEDIS1_ID));
Thread.sleep(1000);
Expand Down Expand Up @@ -362,7 +364,8 @@ public void testInflightCommandsAreNotRetriedAfterFailover() throws Exception {
MultiDbConnectionProvider customProvider = createProvider(
builder -> builder.retryOnFailover(false));

try (UnifiedJedis customClient = new UnifiedJedis(customProvider)) {
try (MultiDbClient customClient = MultiDbClient.builder().connectionProvider(customProvider)
.build()) {

assertThat(getNodeId(customClient.info("server")), equalTo(JEDIS1_ID));
Future<List<String>> blpop = executor.submit(() -> customClient.blpop(500, "test-list-2"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import redis.clients.jedis.scenario.RecommendedSettings;
import redis.clients.jedis.scenario.FaultInjectionClient.TriggerActionResponse;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.util.ClientTestUtil;

import java.io.IOException;
import java.time.Duration;
Expand Down Expand Up @@ -152,13 +153,12 @@ public void accept(DatabaseSwitchEvent e) {
ensureEndpointAvailability(endpoint1.getHostAndPort(), config);
ensureEndpointAvailability(endpoint2.getHostAndPort(), config);

// Create the connection provider
MultiDbConnectionProvider provider = new MultiDbConnectionProvider(builder.build());
FailoverReporter reporter = new FailoverReporter();
provider.setDatabaseSwitchListener(reporter);
provider.setActiveDatabase(endpoint1.getHostAndPort());

UnifiedJedis client = new UnifiedJedis(provider);
MultiDbClient multiDbClient = MultiDbClient.builder().multiDbConfig(builder.build())
.databaseSwitchListener(reporter).build();

multiDbClient.setActiveDatabase(endpoint1.getHostAndPort());

AtomicLong retryingThreadsCounter = new AtomicLong(0);
AtomicLong failedCommandsAfterFailover = new AtomicLong(0);
Expand All @@ -168,33 +168,32 @@ public void accept(DatabaseSwitchEvent e) {
AtomicBoolean unexpectedErrors = new AtomicBoolean(false);
AtomicReference<Exception> lastException = new AtomicReference<Exception>();
AtomicLong stopRunningAt = new AtomicLong();
String database2Id = provider.getDatabase(endpoint2.getHostAndPort()).getCircuitBreaker()
.getName();
Endpoint db2Endpoint = endpoint2.getHostAndPort();

// Start thread that imitates an application that uses the client
// Start thread that imitates an application that uses the multiDbClient
RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom().limitForPeriod(100)
.limitRefreshPeriod(Duration.ofSeconds(1)).timeoutDuration(Duration.ofSeconds(1)).build();

MultiThreadedFakeApp fakeApp = new MultiThreadedFakeApp(client, (UnifiedJedis c) -> {
MultiThreadedFakeApp fakeApp = new MultiThreadedFakeApp(multiDbClient, (UnifiedJedis c) -> {

long threadId = Thread.currentThread().getId();

int attempt = 0;
int maxTries = 500;
int retryingDelay = 5;
String currentDatabaseId = null;
String currentDbKey = null;
while (true) {
try {
if (System.currentTimeMillis() > stopRunningAt.get()) break;
currentDatabaseId = provider.getDatabase().getCircuitBreaker().getName();
currentDbKey = dbKey(multiDbClient.getActiveDatabaseEndpoint());
Map<String, String> executionInfo = new HashMap<String, String>() {
{
put("threadId", String.valueOf(threadId));
put("database", reporter.getCurrentDatabaseName());
}
};

client.xadd("execution_log", StreamEntryID.NEW_ENTRY, executionInfo);
multiDbClient.xadd("execution_log", StreamEntryID.NEW_ENTRY, executionInfo);

if (attempt > 0) {
log.info("Thread {} recovered after {} ms. Threads still not recovered: {}", threadId,
Expand All @@ -203,7 +202,7 @@ public void accept(DatabaseSwitchEvent e) {

break;
} catch (JedisConnectionException e) {
if (database2Id.equals(currentDatabaseId)) {
if (dbKey(db2Endpoint).equals(currentDbKey)) {
break;
}
lastException.set(e);
Expand Down Expand Up @@ -231,7 +230,7 @@ public void accept(DatabaseSwitchEvent e) {
}
if (++attempt == maxTries) throw e;
} catch (Exception e) {
if (database2Id.equals(currentDatabaseId)) {
if (dbKey(db2Endpoint).equals(currentDbKey)) {
break;
}
lastException.set(e);
Expand Down Expand Up @@ -275,6 +274,7 @@ public boolean isCompleted(Duration checkInterval, Duration delayAfter, Duration
}
log.info("Fake app completed");

MultiDbConnectionProvider provider = ClientTestUtil.getConnectionProvider(multiDbClient);
ConnectionPool pool = provider.getDatabase(endpoint1.getHostAndPort()).getConnectionPool();

log.info("First connection pool state: active: {}, idle: {}", pool.getNumActive(),
Expand Down Expand Up @@ -305,7 +305,11 @@ public boolean isCompleted(Duration checkInterval, Duration delayAfter, Duration
}
assertFalse(unexpectedErrors.get());

client.close();
multiDbClient.close();
}

private String dbKey(Endpoint endpoint) {
return endpoint.getHost() + ":" + endpoint.getPort();
}

private static void ensureEndpointAvailability(HostAndPort endpoint, JedisClientConfig config) {
Expand Down
4 changes: 2 additions & 2 deletions src/test/java/redis/clients/jedis/mcf/DefaultValuesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ void testDefaultValuesInConfig() {
// check healthchecks enabled
assertNotNull(databaseConfig.getHealthCheckStrategySupplier());

// check default healthcheck strategy is echo
assertEquals(EchoStrategy.DEFAULT, databaseConfig.getHealthCheckStrategySupplier());
// check default healthcheck strategy is PingStrategy
assertEquals(PingStrategy.DEFAULT, databaseConfig.getHealthCheckStrategySupplier());

// check number of probes
assertEquals(3,
Expand Down
Loading
Loading