Skip to content
Closed
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -522,10 +522,10 @@ stop:
test: | start mvn-test-local stop

mvn-test-local:
@TEST_ENV_PROVIDER=local mvn -Dwith-param-names=true -Dtest=${TEST} clean compile test
@TEST_ENV_PROVIDER=local mvn -Dwith-param-names=true -Dtest=${TEST} clean verify

mvn-test:
mvn -Dwith-param-names=true -Dtest=${TEST} clean compile test
mvn -Dwith-param-names=true -Dtest=${TEST} clean verify

package: | start mvn-package stop

Expand Down
14 changes: 5 additions & 9 deletions docs/failover.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ Then build a `MultiClusterPooledConnectionProvider`.

```java
MultiClusterClientConfig.Builder builder = new MultiClusterClientConfig.Builder(clientConfigs);
builder.circuitBreakerSlidingWindowSize(10); // Sliding window size in number of calls
builder.circuitBreakerSlidingWindowMinCalls(1);
builder.circuitBreakerFailureRateThreshold(50.0f); // percentage of failures to trigger circuit breaker
builder.circuitBreakerSlidingWindowSize(2); // Sliding window size in number of calls
builder.circuitBreakerFailureRateThreshold(10.0f); // percentage of failures to trigger circuit breaker

builder.failbackSupported(true); // Enable failback
builder.failbackCheckInterval(1000); // Check every second the unhealthy cluster to see if it has recovered
Expand Down Expand Up @@ -140,12 +139,9 @@ Jedis uses the following circuit breaker settings:

| Setting | Default value | Description |
|-----------------------------------------|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Sliding window type | `COUNT_BASED` | The type of sliding window used to record the outcome of calls. Options are `COUNT_BASED` and `TIME_BASED`. |
| Sliding window size | 100 | The size of the sliding window. Units depend on sliding window type. When `COUNT_BASED`, the size represents number of calls. When `TIME_BASED`, the size represents seconds. |
| Sliding window min calls | 100 | Minimum number of calls required (per sliding window period) before the CircuitBreaker will start calculating the error rate or slow call rate. |
| Failure rate threshold | `50.0f` | Percentage of calls within the sliding window that must fail before the circuit breaker transitions to the `OPEN` state. |
| Slow call duration threshold | 60000 ms | Duration threshold above which calls are classified as slow and added to the sliding window. |
| Slow call rate threshold | `100.0f` | Percentage of calls within the sliding window that exceed the slow call duration threshold before circuit breaker transitions to the `OPEN` state. |
| Sliding window size | 2 | The size of the sliding window. Units depend on sliding window type. The size represents seconds. |
| Threshold min number of failures | 1000 | Minimum number of failures before circuit breaker is tripped. |
| Failure rate threshold | `10.0f` | Percentage of calls within the sliding window that must fail before the circuit breaker transitions to the `OPEN` state. |
| Circuit breaker included exception list | [JedisConnectionException] | A list of Throwable classes that count as failures and add to the failure rate. |
| Circuit breaker ignored exception list | null | A list of Throwable classes to explicitly ignore for failure rate calculations. | |

Expand Down
19 changes: 8 additions & 11 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
<junit.version>5.13.4</junit.version>
<!-- Default JVM options for tests -->
<JVM_OPTS></JVM_OPTS>
<!-- Default excluded groups for tests - can be overridden from command line -->
<excludedGroupsForUnitTests>integration,scenario</excludedGroupsForUnitTests>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -335,7 +337,7 @@
<systemPropertyVariables>
<redis-hosts>${redis-hosts}</redis-hosts>
</systemPropertyVariables>
<excludedGroups>integration,scenario</excludedGroups>
<excludedGroups>${excludedGroupsForUnitTests}</excludedGroups>
<excludes>
<exclude>**/examples/*.java</exclude>
<exclude>**/scenario/*Test.java</exclude>
Expand Down Expand Up @@ -482,21 +484,16 @@
<include>**/Endpoint.java</include>
<include>src/main/java/redis/clients/jedis/mcf/*.java</include>
<include>src/test/java/redis/clients/jedis/failover/*.java</include>
<include>**/mcf/EchoStrategyIntegrationTest.java</include>
<include>**/mcf/LagAwareStrategyUnitTest.java</include>
<include>**/mcf/RedisRestAPI*.java</include>
<include>**/mcf/ActiveActiveLocalFailoverTest*</include>
<include>**/mcf/FailbackMechanism*.java</include>
<include>**/mcf/PeriodicFailbackTest*.java</include>
<include>**/mcf/AutomaticFailoverTest*.java</include>
<include>**/mcf/MultiCluster*.java</include>
<include>**/mcf/StatusTracker*.java</include>
<include>src/test/java/redis/clients/jedis/mcf/*.java</include>
<include>**/Health*.java</include>
<include>**/*IT.java</include>
<include>**/scenario/RestEndpointUtil.java</include>
<include>src/main/java/redis/clients/jedis/MultiClusterClientConfig.java</include>
<include>src/main/java/redis/clients/jedis/MultiDbConfig.java</include>
<include>src/main/java/redis/clients/jedis/HostAndPort.java</include>
<include>**/builders/*.java</include>
<include>**/MultiDb*.java</include>
<include>**/ClientTestUtil.java</include>
<include>**/ReflectionTestUtil.java</include>
</includes>
</configuration>
<executions>
Expand Down
282 changes: 282 additions & 0 deletions src/main/java/redis/clients/jedis/MultiDbClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package redis.clients.jedis;

import redis.clients.jedis.MultiDbConfig.DatabaseConfig;
import redis.clients.jedis.annots.Experimental;
import redis.clients.jedis.builders.MultiDbClientBuilder;
import redis.clients.jedis.csc.Cache;
import redis.clients.jedis.executors.CommandExecutor;
import redis.clients.jedis.mcf.MultiDbCommandExecutor;
import redis.clients.jedis.mcf.MultiDbPipeline;
import redis.clients.jedis.mcf.MultiDbTransaction;
import redis.clients.jedis.providers.ConnectionProvider;
import redis.clients.jedis.mcf.MultiDbConnectionProvider;

import java.util.Set;

/**
* MultiDbClient provides high-availability Redis connectivity with automatic failover and failback
* capabilities across multiple weighted endpoints.
* <p>
* This client extends UnifiedJedis to support resilient operations with:
* <ul>
* <li><strong>Multi-Endpoint Support:</strong> Configure multiple Redis endpoints with individual
* weights</li>
* <li><strong>Automatic Failover:</strong> Seamless switching to backup endpoints when primary
* becomes unavailable</li>
* <li><strong>Circuit Breaker Pattern:</strong> Built-in circuit breaker to prevent cascading
* failures</li>
* <li><strong>Weight-Based Selection:</strong> Intelligent endpoint selection based on configured
* weights</li>
* <li><strong>Health Monitoring:</strong> Continuous health checks with automatic failback to
* recovered endpoints</li>
* <li><strong>Retry Logic:</strong> Configurable retry mechanisms with exponential backoff</li>
* </ul>
* <p>
* <strong>Usage Example:</strong>
* </p>
*
* <pre>
* // Create multi-db client with multiple endpoints
* HostAndPort primary = new HostAndPort("localhost", 29379);
* HostAndPort secondary = new HostAndPort("localhost", 29380);
*
*
* MultiDbClient client = MultiDbClient.builder()
* .multiDbConfig(
* MultiDbConfig.builder()
* .endpoint(
* DatabaseConfig.builder(
* primary,
* DefaultJedisClientConfig.builder().build())
* .weight(100.0f)
* .build())
* .endpoint(DatabaseConfig.builder(
* secondary,
* DefaultJedisClientConfig.builder().build())
* .weight(50.0f).build())
* .circuitBreakerFailureRateThreshold(50.0f)
* .retryMaxAttempts(3)
* .build()
* )
* .databaseSwitchListener(event -&gt;
* System.out.println("Switched to: " + event.getEndpoint()))
* .build();
*
* // Use like any other Jedis client
* client.set("key", "value");
* String value = client.get("key");
*
* // Automatic failover happens transparently
* client.close();
* </pre>
* <p>
* The client automatically handles endpoint failures and recoveries, providing transparent high
* availability for Redis operations. All standard Jedis operations are supported with the added
* resilience features.
* </p>
* @author Ivo Gaydazhiev
* @since 7.0.0
* @see MultiDbConnectionProvider
* @see MultiDbCommandExecutor
* @see MultiDbConfig
*/
@Experimental
public class MultiDbClient extends UnifiedJedis {

/**
* Creates a MultiDbClient with custom components.
* <p>
* This constructor allows full customization of the client components and is primarily used by
* the builder pattern for advanced configurations. For most use cases, prefer using
* {@link #builder()} to create instances.
* </p>
* @param commandExecutor the command executor (typically MultiDbCommandExecutor)
* @param connectionProvider the connection provider (typically MultiDbConnectionProvider)
* @param commandObjects the command objects
* @param redisProtocol the Redis protocol version
* @param cache the client-side cache (may be null)
*/
MultiDbClient(CommandExecutor commandExecutor, ConnectionProvider connectionProvider,
CommandObjects commandObjects, RedisProtocol redisProtocol, Cache cache) {
super(commandExecutor, connectionProvider, commandObjects, redisProtocol, cache);
}

/**
* Returns the underlying MultiDbConnectionProvider.
* <p>
* This provides access to multi-cluster specific operations like manual failover, health status
* monitoring, and cluster switch event handling.
* </p>
* @return the multi-cluster connection provider
* @throws ClassCastException if the provider is not a MultiDbConnectionProvider
*/
private MultiDbConnectionProvider getMultiDbConnectionProvider() {
return (MultiDbConnectionProvider) this.provider;
}

/**
* Manually switches to the specified endpoint.
* <p>
* This method allows manual failover to a specific endpoint, bypassing the automatic weight-based
* selection. The switch will only succeed if the target endpoint is healthy.
* </p>
* @param endpoint the endpoint to switch to
*/
public void setActiveDatabase(Endpoint endpoint) {
getMultiDbConnectionProvider().setActiveDatabase(endpoint);
}

/**
* Adds a pre-configured cluster configuration.
* <p>
* This method allows adding a fully configured DatabaseConfig instance, providing maximum
* flexibility for advanced configurations including custom health check strategies, connection
* pool settings, etc.
* </p>
* @param databaseConfig the pre-configured database configuration
*/
public void addEndpoint(DatabaseConfig databaseConfig) {
getMultiDbConnectionProvider().add(databaseConfig);
}

/**
* Dynamically adds a new cluster endpoint to the resilient client.
* <p>
* This allows adding new endpoints at runtime without recreating the client. The new endpoint
* will be available for failover operations immediately after being added and passing health
* checks (if configured).
* </p>
* @param endpoint the Redis server endpoint
* @param weight the weight for this endpoint (higher values = higher priority)
* @param clientConfig the client configuration for this endpoint
* @throws redis.clients.jedis.exceptions.JedisValidationException if the endpoint already exists
*/
public void addEndpoint(Endpoint endpoint, float weight, JedisClientConfig clientConfig) {
DatabaseConfig databaseConfig = DatabaseConfig.builder(endpoint, clientConfig).weight(weight)
.build();

getMultiDbConnectionProvider().add(databaseConfig);
}

/**
* Returns the set of all configured endpoints.
* <p>
* This method provides a view of all endpoints currently configured in the resilient client.
* </p>
* @return the set of all configured endpoints
*/
public Set<Endpoint> getEndpoints() {
return getMultiDbConnectionProvider().getEndpoints();
}

/**
* Returns the health status of the specified endpoint.
* <p>
* This method provides the current health status of a specific endpoint.
* </p>
* @param endpoint the endpoint to check
* @return the health status of the endpoint
*/
public boolean isHealthy(Endpoint endpoint) {
return getMultiDbConnectionProvider().isHealthy(endpoint);
}

/**
* Dynamically removes a cluster endpoint from the resilient client.
* <p>
* This allows removing endpoints at runtime. If the removed endpoint is currently active, the
* client will automatically failover to the next available healthy endpoint based on weight
* priority.
* </p>
* @param endpoint the endpoint to remove
* @throws redis.clients.jedis.exceptions.JedisValidationException if the endpoint doesn't exist
* @throws redis.clients.jedis.exceptions.JedisException if removing the endpoint would leave no
* healthy clusters available
*/
public void removeEndpoint(Endpoint endpoint) {
getMultiDbConnectionProvider().remove(endpoint);
}

/**
* Forces the client to switch to a specific endpoint for a duration.
* <p>
* This method forces the client to use the specified endpoint and puts all other endpoints in a
* grace period, preventing automatic failover for the specified duration. This is useful for
* maintenance scenarios or testing specific endpoints.
* </p>
* @param endpoint the endpoint to force as active
* @param forcedActiveDurationMs the duration in milliseconds to keep this endpoint forced
* @throws redis.clients.jedis.exceptions.JedisValidationException if the endpoint is not healthy
* or doesn't exist
*/
public void forceActiveEndpoint(Endpoint endpoint, long forcedActiveDurationMs) {
getMultiDbConnectionProvider().forceActiveDatabase(endpoint, forcedActiveDurationMs);
}

/**
* Creates a new pipeline for batch operations with multi-cluster support.
* <p>
* The returned pipeline supports the same resilience features as the main client, including
* automatic failover during batch execution.
* </p>
* @return a new MultiDbPipeline instance
*/
@Override
public MultiDbPipeline pipelined() {
return new MultiDbPipeline(getMultiDbConnectionProvider(), commandObjects);
}

/**
* Creates a new transaction with multi-cluster support.
* <p>
* The returned transaction supports the same resilience features as the main client, including
* automatic failover during transaction execution.
* </p>
* @return a new MultiDbTransaction instance
*/
@Override
public MultiDbTransaction multi() {
return new MultiDbTransaction((MultiDbConnectionProvider) provider, true, commandObjects);
}

/**
* @param doMulti {@code false} should be set to enable manual WATCH, UNWATCH and MULTI
* @return transaction object
*/
@Override
public MultiDbTransaction transaction(boolean doMulti) {
if (provider == null) {
throw new IllegalStateException(
"It is not allowed to create Transaction from this " + getClass());
}

return new MultiDbTransaction(getMultiDbConnectionProvider(), doMulti, commandObjects);
}

public Endpoint getActiveEndpoint() {
return getMultiDbConnectionProvider().getDatabase().getEndpoint();
}

/**
* Fluent builder for {@link MultiDbClient}.
* <p>
* Obtain an instance via {@link #builder()}.
* </p>
*/
public static class Builder extends MultiDbClientBuilder<MultiDbClient> {

@Override
protected MultiDbClient createClient() {
return new MultiDbClient(commandExecutor, connectionProvider, commandObjects,
clientConfig.getRedisProtocol(), cache);
}
}

/**
* Create a new builder for configuring MultiDbClient instances.
* @return a new {@link MultiDbClient.Builder} instance
*/
public static Builder builder() {
return new Builder();
}
}
Loading