Skip to content

Commit c426e81

Browse files
atakavciCopilot
andauthored
[automatic failover] Introduce fast failover mode - a thread-sync-free approach with builders (#4226)
* - weighted cluster seleciton - Healtstatus manager with initial listener and registration logic - pluggable health checker strategy introduced, these are draft NoOpStrategy, EchoStrategy, LagAwareStrategy, - fix failing tests impacted from weighted clusters * - add builder for ClusterConfig - add echo ot CommandObjects and UnifiedJEdis - improve StrategySupplier by accepting jedisclientconfig - adapt EchoStrategy to StrategySupplier. Now it handles the creation of connection by accepting endpoint and JedisClientConfig - make healthchecks disabled by default - drop noOpStrategy - add unit&integration tests for health check * - fix naming * clean up and mark override methods * fix link in javadoc * fix formatting * - fix double registered listeners in healtstatusmgr - clear redundant catch - replace failover options and drop failoveroptions class - remove forced_unhealthy from healthstatus - fix failback check - add disabled flag to cluster - update/fix related tests * Update src/main/java/redis/clients/jedis/mcf/EchoStrategy.java Co-authored-by: Copilot <[email protected]> * - add remove endpoints * - replace cluster disabled with failbackCandidate - replace failback enabled with failbacksupported in client - fix formatting - set defaults * - remove failback candidate - fix failing tests * - fix remove logic - fix failing tests * - periodic failback checks - introduce graceperiod - fix issue when CB is forced_open and gracePeriod is completed * - introduce StatusTracker with purpose of waiting initial healthcheck results during consturction of provider - add HealthStatus.UNKNOWN as default for Cluster - handle status changes in order of events during initialization - add tests for status tracker and orderingof events - fix impacted unit&integ tests * - introduce forceActiveCluster by duration - fix formatting * - fix failing tests by waiting on clusters to get healthy * - fix failing scenario test - downgrade logback version for slf4j compatibility - increase timeouts for faultInjector * - adressing reviews and feedback * - fix formatting * - fix formatting * - get rid of the queue and event ordering for healthstatus change in MultiClusterPooledConnectionProvider - add test for init and post init events - fix failing tests * - replace use of reflection with helper methods - fix failing tests due to method name change * - introduce clusterSwitchEvent and drop clusterFailover post processor - fix broken echostrategy due to connection issue - make healtthCheckStrategy closable and close on - adding fastfailover mode to config and provider - add local failover tests for total failover duration * - introduce fastfailover using objectMaker injection into connectionFactory * - polish * - cleanup * - improve healtcheck thread visibility * - introduce TrackingConnectionPool with FailFastConnectionFactory - added builders to connection and connectionFactory - introduce initializtionTracker to track list of connections during their construction. * - return broken source as usual - do not throw exception is failover already happening * - unblock waiting threads * - failover by closing the pool * - formatting * - check waiters and active/idle connections to force disconnect * - add builder to trackingconnectionpool * - fix failing tests due to mocked ctor for trackingConnectionPool * - replace initTracker with split initializtion of conn * - refactor on builders and ctors * - undo format * - clena up * - add exceptions to logs * - add max wait duration and minConsecutiveSuccessCount to healthCheckStrategy * - replace 'sleep' with 'await', feedback from @a-TODO-rov * - fix status tracker tests --------- Co-authored-by: Copilot <[email protected]>
1 parent c8c0b01 commit c426e81

27 files changed

+1037
-181
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ tags
1717
redis-git
1818
appendonlydir/
1919
.DS_Store
20+
.vscode/settings.json

src/main/java/redis/clients/jedis/Connection.java

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,39 @@
3333

3434
public class Connection implements Closeable {
3535

36+
public static class Builder {
37+
private JedisSocketFactory socketFactory;
38+
private JedisClientConfig clientConfig;
39+
40+
public Builder socketFactory(JedisSocketFactory socketFactory) {
41+
this.socketFactory = socketFactory;
42+
return this;
43+
}
44+
45+
public Builder clientConfig(JedisClientConfig clientConfig) {
46+
this.clientConfig = clientConfig;
47+
return this;
48+
}
49+
50+
public JedisSocketFactory getSocketFactory() {
51+
return socketFactory;
52+
}
53+
54+
public JedisClientConfig getClientConfig() {
55+
return clientConfig;
56+
}
57+
58+
public Connection build() {
59+
Connection conn = new Connection(this);
60+
conn.initializeFromClientConfig();
61+
return conn;
62+
}
63+
}
64+
65+
public static Builder builder() {
66+
return new Builder();
67+
}
68+
3669
private ConnectionPool memberOf;
3770
protected RedisProtocol protocol;
3871
private final JedisSocketFactory socketFactory;
@@ -48,6 +81,7 @@ public class Connection implements Closeable {
4881
protected String version;
4982
private AtomicReference<RedisCredentials> currentCredentials = new AtomicReference<>(null);
5083
private AuthXManager authXManager;
84+
private JedisClientConfig clientConfig;
5185

5286
public Connection() {
5387
this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT);
@@ -67,16 +101,19 @@ public Connection(final HostAndPort hostAndPort, final JedisClientConfig clientC
67101

68102
public Connection(final JedisSocketFactory socketFactory) {
69103
this.socketFactory = socketFactory;
70-
this.authXManager = null;
71104
}
72105

73106
public Connection(final JedisSocketFactory socketFactory, JedisClientConfig clientConfig) {
74107
this.socketFactory = socketFactory;
75-
this.soTimeout = clientConfig.getSocketTimeoutMillis();
76-
this.infiniteSoTimeout = clientConfig.getBlockingSocketTimeoutMillis();
108+
this.clientConfig = clientConfig;
77109
initializeFromClientConfig(clientConfig);
78110
}
79111

112+
protected Connection(Builder builder) {
113+
this.socketFactory = builder.getSocketFactory();
114+
this.clientConfig = builder.getClientConfig();
115+
}
116+
80117
@Override
81118
public String toString() {
82119
return getClass().getSimpleName() + "{" + socketFactory + "}";
@@ -288,6 +325,10 @@ public void disconnect() {
288325
}
289326
}
290327

328+
public void forceDisconnect() throws IOException {
329+
socket.close();
330+
}
331+
291332
public boolean isConnected() {
292333
return socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()
293334
&& !socket.isInputShutdown() && !socket.isOutputShutdown();
@@ -450,8 +491,15 @@ private static boolean validateClientInfo(String info) {
450491
return true;
451492
}
452493

494+
public void initializeFromClientConfig() {
495+
this.initializeFromClientConfig(clientConfig);
496+
}
497+
453498
protected void initializeFromClientConfig(final JedisClientConfig config) {
454499
try {
500+
this.soTimeout = config.getSocketTimeoutMillis();
501+
this.infiniteSoTimeout = config.getBlockingSocketTimeoutMillis();
502+
455503
connect();
456504

457505
protocol = config.getRedisProtocol();

src/main/java/redis/clients/jedis/ConnectionFactory.java

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,56 +21,137 @@
2121
*/
2222
public class ConnectionFactory implements PooledObjectFactory<Connection> {
2323

24+
public static class Builder {
25+
private JedisClientConfig clientConfig;
26+
private Connection.Builder connectionBuilder;
27+
private JedisSocketFactory jedisSocketFactory;
28+
private Cache cache;
29+
private HostAndPort hostAndPort;
30+
31+
// Fluent API methods (preferred)
32+
public Builder clientConfig(JedisClientConfig clientConfig) {
33+
this.clientConfig = clientConfig;
34+
return this;
35+
}
36+
37+
public Builder connectionBuilder(Connection.Builder connectionBuilder) {
38+
this.connectionBuilder = connectionBuilder;
39+
return this;
40+
}
41+
42+
public Builder socketFactory(JedisSocketFactory jedisSocketFactory) {
43+
this.jedisSocketFactory = jedisSocketFactory;
44+
return this;
45+
}
46+
47+
public Builder cache(Cache cache) {
48+
this.cache = cache;
49+
return this;
50+
}
51+
52+
public Builder hostAndPort(HostAndPort hostAndPort) {
53+
this.hostAndPort = hostAndPort;
54+
return this;
55+
}
56+
57+
public Connection.Builder getConnectionBuilder() {
58+
return connectionBuilder;
59+
}
60+
61+
public JedisSocketFactory getJedisSocketFactory() {
62+
return jedisSocketFactory;
63+
}
64+
65+
public JedisClientConfig getClientConfig() {
66+
return clientConfig;
67+
}
68+
69+
public Cache getCache() {
70+
return cache;
71+
}
72+
73+
public ConnectionFactory build() {
74+
withDefaults();
75+
return new ConnectionFactory(this);
76+
}
77+
78+
private Builder withDefaults() {
79+
if (jedisSocketFactory == null) {
80+
this.jedisSocketFactory = createDefaultSocketFactory();
81+
}
82+
if (connectionBuilder == null) {
83+
this.connectionBuilder = createDefaultConnectionBuilder();
84+
}
85+
return this;
86+
}
87+
88+
private JedisSocketFactory createDefaultSocketFactory() {
89+
if (clientConfig == null) {
90+
clientConfig = DefaultJedisClientConfig.builder().build();
91+
}
92+
if (hostAndPort == null) {
93+
throw new IllegalStateException("HostAndPort is required when no socketFactory is provided");
94+
}
95+
return new DefaultJedisSocketFactory(hostAndPort, clientConfig);
96+
}
97+
98+
private Connection.Builder createDefaultConnectionBuilder() {
99+
Connection.Builder connBuilder = cache == null ? Connection.builder() : CacheConnection.builder(cache);
100+
connBuilder.socketFactory(jedisSocketFactory).clientConfig(clientConfig);
101+
return connBuilder;
102+
}
103+
}
104+
105+
public static Builder builder() {
106+
return new Builder();
107+
}
108+
24109
private static final Logger logger = LoggerFactory.getLogger(ConnectionFactory.class);
25110

26-
private final JedisSocketFactory jedisSocketFactory;
27111
private final JedisClientConfig clientConfig;
28-
private final Cache clientSideCache;
29-
private final Supplier<Connection> objectMaker;
112+
private Supplier<Connection> objectMaker;
113+
private Connection.Builder connectionBuilder;
30114

31-
private final AuthXEventListener authXEventListener;
115+
private AuthXEventListener authXEventListener;
32116

33117
public ConnectionFactory(final HostAndPort hostAndPort) {
34-
this(hostAndPort, DefaultJedisClientConfig.builder().build(), null);
118+
this(builder().hostAndPort(hostAndPort).withDefaults());
35119
}
36120

37121
public ConnectionFactory(final HostAndPort hostAndPort, final JedisClientConfig clientConfig) {
38-
this(hostAndPort, clientConfig, null);
122+
this(builder().hostAndPort(hostAndPort).clientConfig(clientConfig).withDefaults());
39123
}
40124

41125
@Experimental
42-
public ConnectionFactory(final HostAndPort hostAndPort, final JedisClientConfig clientConfig,
43-
Cache csCache) {
44-
this(new DefaultJedisSocketFactory(hostAndPort, clientConfig), clientConfig, csCache);
126+
public ConnectionFactory(final HostAndPort hostAndPort, final JedisClientConfig clientConfig, Cache csCache) {
127+
this(builder().hostAndPort(hostAndPort).clientConfig(clientConfig).cache(csCache).withDefaults());
45128
}
46129

47-
public ConnectionFactory(final JedisSocketFactory jedisSocketFactory,
48-
final JedisClientConfig clientConfig) {
49-
this(jedisSocketFactory, clientConfig, null);
130+
public ConnectionFactory(final JedisSocketFactory jedisSocketFactory, final JedisClientConfig clientConfig) {
131+
this(builder().socketFactory(jedisSocketFactory).clientConfig(clientConfig).withDefaults());
50132
}
51133

52-
private ConnectionFactory(final JedisSocketFactory jedisSocketFactory,
53-
final JedisClientConfig clientConfig, Cache csCache) {
134+
public ConnectionFactory(Builder builder) {
135+
this.clientConfig = builder.getClientConfig();
136+
this.connectionBuilder = builder.getConnectionBuilder();
54137

55-
this.jedisSocketFactory = jedisSocketFactory;
56-
this.clientSideCache = csCache;
57-
this.clientConfig = clientConfig;
138+
initAuthXManager();
139+
}
58140

141+
private void initAuthXManager() {
59142
AuthXManager authXManager = clientConfig.getAuthXManager();
60143
if (authXManager == null) {
61-
this.objectMaker = connectionSupplier();
144+
this.objectMaker = () -> build();
62145
this.authXEventListener = AuthXEventListener.NOOP_LISTENER;
63146
} else {
64-
Supplier<Connection> supplier = connectionSupplier();
65-
this.objectMaker = () -> (Connection) authXManager.addConnection(supplier.get());
147+
this.objectMaker = () -> (Connection) authXManager.addConnection(build());
66148
this.authXEventListener = authXManager.getListener();
67149
authXManager.start();
68150
}
69151
}
70152

71-
private Supplier<Connection> connectionSupplier() {
72-
return clientSideCache == null ? () -> new Connection(jedisSocketFactory, clientConfig)
73-
: () -> new CacheConnection(jedisSocketFactory, clientConfig, clientSideCache);
153+
private Connection build() {
154+
return connectionBuilder.build();
74155
}
75156

76157
@Override

src/main/java/redis/clients/jedis/ConnectionPool.java

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.apache.commons.pool2.PooledObjectFactory;
44
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
55

6+
import redis.clients.authentication.core.Token;
67
import redis.clients.jedis.annots.Experimental;
78
import redis.clients.jedis.authentication.AuthXManager;
89
import redis.clients.jedis.csc.Cache;
@@ -13,25 +14,32 @@ public class ConnectionPool extends Pool<Connection> {
1314

1415
private AuthXManager authXManager;
1516

17+
// Primary constructors using factory
18+
public ConnectionPool(PooledObjectFactory<Connection> factory) {
19+
super(factory);
20+
}
21+
22+
public ConnectionPool(PooledObjectFactory<Connection> factory,
23+
GenericObjectPoolConfig<Connection> poolConfig) {
24+
super(factory, poolConfig);
25+
}
26+
27+
// Convenience constructors
1628
public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig) {
1729
this(new ConnectionFactory(hostAndPort, clientConfig));
1830
attachAuthenticationListener(clientConfig.getAuthXManager());
1931
}
2032

21-
@Experimental
2233
public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig,
23-
Cache clientSideCache) {
24-
this(new ConnectionFactory(hostAndPort, clientConfig, clientSideCache));
34+
GenericObjectPoolConfig<Connection> poolConfig) {
35+
this(new ConnectionFactory(hostAndPort, clientConfig), poolConfig);
2536
attachAuthenticationListener(clientConfig.getAuthXManager());
2637
}
2738

28-
public ConnectionPool(PooledObjectFactory<Connection> factory) {
29-
super(factory);
30-
}
31-
39+
@Experimental
3240
public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig,
33-
GenericObjectPoolConfig<Connection> poolConfig) {
34-
this(new ConnectionFactory(hostAndPort, clientConfig), poolConfig);
41+
Cache clientSideCache) {
42+
this(new ConnectionFactory(hostAndPort, clientConfig, clientSideCache));
3543
attachAuthenticationListener(clientConfig.getAuthXManager());
3644
}
3745

@@ -42,11 +50,6 @@ public ConnectionPool(HostAndPort hostAndPort, JedisClientConfig clientConfig,
4250
attachAuthenticationListener(clientConfig.getAuthXManager());
4351
}
4452

45-
public ConnectionPool(PooledObjectFactory<Connection> factory,
46-
GenericObjectPoolConfig<Connection> poolConfig) {
47-
super(factory, poolConfig);
48-
}
49-
5053
@Override
5154
public Connection getResource() {
5255
Connection conn = super.getResource();
@@ -65,17 +68,25 @@ public void close() {
6568
}
6669
}
6770

68-
private void attachAuthenticationListener(AuthXManager authXManager) {
71+
protected void attachAuthenticationListener(AuthXManager authXManager) {
6972
this.authXManager = authXManager;
7073
if (authXManager != null) {
71-
authXManager.addPostAuthenticationHook(token -> {
72-
try {
73-
// this is to trigger validations on each connection via ConnectionFactory
74-
evict();
75-
} catch (Exception e) {
76-
throw new JedisException("Failed to evict connections from pool", e);
77-
}
78-
});
74+
authXManager.addPostAuthenticationHook(this::postAuthentication);
75+
}
76+
}
77+
78+
protected void detachAuthenticationListener() {
79+
if (authXManager != null) {
80+
authXManager.removePostAuthenticationHook(this::postAuthentication);
81+
}
82+
}
83+
84+
private void postAuthentication(Token token) {
85+
try {
86+
// this is to trigger validations on each connection via ConnectionFactory
87+
evict();
88+
} catch (Exception e) {
89+
throw new JedisException("Failed to evict connections from pool", e);
7990
}
8091
}
8192
}

0 commit comments

Comments
 (0)