Skip to content

Commit b6fbc4e

Browse files
authored
[automatic-failover] Support for dynamic add/remove endpoints (#3517)
* - Add/Remove databases safely * - secure switchToDatabase * - guard listeners and db switch against race conditions. * feedbacks from @ggivo - add close to both MultiDbConnection and CircuitBreaker - skip switchToDatabase when source and destination is same db * - add test around attempt to switch to same db
1 parent bec5f28 commit b6fbc4e

File tree

5 files changed

+239
-57
lines changed

5 files changed

+239
-57
lines changed

src/main/java/io/lettuce/core/failover/CircuitBreaker.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.lettuce.core.failover;
22

3+
import java.io.Closeable;
34
import java.io.IOException;
45
import java.net.ConnectException;
56
import java.util.Arrays;
@@ -25,7 +26,7 @@
2526
* @author Ali Takavci
2627
* @since 7.1
2728
*/
28-
public class CircuitBreaker {
29+
public class CircuitBreaker implements Closeable {
2930

3031
private static final Logger log = LoggerFactory.getLogger(CircuitBreaker.class);
3132

@@ -134,6 +135,11 @@ private void fireStateChanged(State previousState, State newState) {
134135
}
135136
}
136137

138+
@Override
139+
public void close() {
140+
listeners.clear();
141+
}
142+
137143
public static enum State {
138144
CLOSED, OPEN
139145
}

src/main/java/io/lettuce/core/failover/RedisDatabase.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.lettuce.core.failover;
22

3+
import java.io.Closeable;
4+
35
import io.lettuce.core.RedisURI;
46
import io.lettuce.core.api.StatefulRedisConnection;
57
import io.lettuce.core.failover.CircuitBreaker.CircuitBreakerConfig;
@@ -12,7 +14,7 @@
1214
* @author Ali Takavci
1315
* @since 7.1
1416
*/
15-
public class RedisDatabase<C extends StatefulRedisConnection<?, ?>> {
17+
public class RedisDatabase<C extends StatefulRedisConnection<?, ?>> implements Closeable {
1618

1719
private final float weight;
1820

@@ -53,6 +55,12 @@ public CircuitBreaker getCircuitBreaker() {
5355
return circuitBreaker;
5456
}
5557

58+
@Override
59+
public void close() {
60+
connection.close();
61+
circuitBreaker.close();
62+
}
63+
5664
public static class RedisDatabaseConfig {
5765

5866
private RedisURI redisURI;

src/main/java/io/lettuce/core/failover/StatefulRedisMultiDbConnectionImpl.java

Lines changed: 91 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import java.util.Set;
99
import java.util.concurrent.CompletableFuture;
1010
import java.util.concurrent.ConcurrentHashMap;
11+
import java.util.concurrent.locks.Lock;
12+
import java.util.concurrent.locks.ReadWriteLock;
13+
import java.util.concurrent.locks.ReentrantReadWriteLock;
1114
import java.util.function.Supplier;
1215

1316
import io.lettuce.core.AbstractRedisClient;
@@ -61,6 +64,12 @@ public class StatefulRedisMultiDbConnectionImpl<C extends StatefulRedisConnectio
6164

6265
protected final DatabaseConnectionFactory<C, K, V> connectionFactory;
6366

67+
private final ReadWriteLock multiDbLock = new ReentrantReadWriteLock();
68+
69+
private final Lock readLock = multiDbLock.readLock();
70+
71+
private final Lock writeLock = multiDbLock.writeLock();
72+
6473
public StatefulRedisMultiDbConnectionImpl(Map<RedisURI, RedisDatabase<C>> connections, ClientResources resources,
6574
RedisCodec<K, V> codec, Supplier<JsonParser> parser, DatabaseConnectionFactory<C, K, V> connectionFactory) {
6675
if (connections == null || connections.isEmpty()) {
@@ -151,14 +160,18 @@ public RedisCommands<K, V> sync() {
151160

152161
@Override
153162
public void addListener(RedisConnectionStateListener listener) {
154-
connectionStateListeners.add(listener);
155-
current.getConnection().addListener(listener);
163+
doBySharedLock(() -> {
164+
connectionStateListeners.add(listener);
165+
current.getConnection().addListener(listener);
166+
});
156167
}
157168

158169
@Override
159170
public void removeListener(RedisConnectionStateListener listener) {
160-
connectionStateListeners.remove(listener);
161-
current.getConnection().removeListener(listener);
171+
doBySharedLock(() -> {
172+
connectionStateListeners.remove(listener);
173+
current.getConnection().removeListener(listener);
174+
});
162175
}
163176

164177
@Override
@@ -224,14 +237,18 @@ public boolean isMulti() {
224237

225238
@Override
226239
public void addListener(PushListener listener) {
227-
pushListeners.add(listener);
228-
current.getConnection().addListener(listener);
240+
doBySharedLock(() -> {
241+
pushListeners.add(listener);
242+
current.getConnection().addListener(listener);
243+
});
229244
}
230245

231246
@Override
232247
public void removeListener(PushListener listener) {
233-
pushListeners.remove(listener);
234-
current.getConnection().removeListener(listener);
248+
doBySharedLock(() -> {
249+
pushListeners.remove(listener);
250+
current.getConnection().removeListener(listener);
251+
});
235252
}
236253

237254
@Override
@@ -251,21 +268,45 @@ public Iterable<RedisURI> getEndpoints() {
251268

252269
@Override
253270
public void switchToDatabase(RedisURI redisURI) {
254-
RedisDatabase<C> fromDb = current;
255-
RedisDatabase<C> toDb = databases.get(redisURI);
256-
if (fromDb == null || toDb == null) {
257-
throw new UnsupportedOperationException("Cannot initiate switch without a current and target database!");
258-
}
259-
current = toDb;
260-
connectionStateListeners.forEach(listener -> {
261-
toDb.getConnection().addListener(listener);
262-
fromDb.getConnection().removeListener(listener);
263-
});
264-
pushListeners.forEach(listener -> {
265-
toDb.getConnection().addListener(listener);
266-
fromDb.getConnection().removeListener(listener);
271+
doByExclusiveLock(() -> {
272+
RedisDatabase<C> fromDb = current;
273+
RedisDatabase<C> toDb = databases.get(redisURI);
274+
if (fromDb == null || toDb == null) {
275+
throw new UnsupportedOperationException(
276+
"Unable to switch between endpoints - the driver was not able to locate the source or destination endpoint.");
277+
}
278+
if (fromDb.equals(toDb)) {
279+
return;
280+
}
281+
current = toDb;
282+
connectionStateListeners.forEach(listener -> {
283+
toDb.getConnection().addListener(listener);
284+
fromDb.getConnection().removeListener(listener);
285+
});
286+
pushListeners.forEach(listener -> {
287+
toDb.getConnection().addListener(listener);
288+
fromDb.getConnection().removeListener(listener);
289+
});
290+
fromDb.getDatabaseEndpoint().handOverCommandQueue(toDb.getDatabaseEndpoint());
267291
});
268-
fromDb.getDatabaseEndpoint().handOverCommandQueue(toDb.getDatabaseEndpoint());
292+
}
293+
294+
protected void doBySharedLock(Runnable operation) {
295+
readLock.lock();
296+
try {
297+
operation.run();
298+
} finally {
299+
readLock.unlock();
300+
}
301+
}
302+
303+
protected void doByExclusiveLock(Runnable operation) {
304+
writeLock.lock();
305+
try {
306+
operation.run();
307+
} finally {
308+
writeLock.unlock();
309+
}
269310
}
270311

271312
@Override
@@ -294,39 +335,44 @@ public void addDatabase(DatabaseConfig databaseConfig) {
294335
}
295336

296337
RedisURI redisURI = databaseConfig.getRedisURI();
297-
if (databases.containsKey(redisURI)) {
298-
throw new IllegalArgumentException("Database already exists: " + redisURI);
299-
}
300338

301-
// Create new database connection using the factory
302-
RedisDatabase<C> database = connectionFactory.createDatabase(databaseConfig, codec);
339+
doByExclusiveLock(() -> {
340+
if (databases.containsKey(redisURI)) {
341+
throw new IllegalArgumentException("Database already exists: " + redisURI);
342+
}
343+
344+
// Create new database connection using the factory
345+
RedisDatabase<C> database = connectionFactory.createDatabase(databaseConfig, codec);
303346

304-
// Add listeners to the new connection if it's the current one
305-
// (though it won't be current initially since we're just adding it)
306-
databases.put(redisURI, database);
347+
// Add listeners to the new connection if it's the current one
348+
// (though it won't be current initially since we're just adding it)
349+
databases.put(redisURI, database);
350+
351+
database.getCircuitBreaker().addListener(this::onCircuitBreakerStateChange);
352+
});
307353

308-
database.getCircuitBreaker().addListener(this::onCircuitBreakerStateChange);
309354
}
310355

311356
@Override
312357
public void removeDatabase(RedisURI redisURI) {
313358
if (redisURI == null) {
314359
throw new IllegalArgumentException("RedisURI must not be null");
315360
}
316-
317-
RedisDatabase<C> database = databases.get(redisURI);
318-
if (database == null) {
319-
throw new IllegalArgumentException("Database not found: " + redisURI);
320-
}
321-
322-
if (current.getRedisURI().equals(redisURI)) {
323-
throw new UnsupportedOperationException("Cannot remove the currently active database: " + redisURI);
324-
}
325-
326-
// Remove the database and close its connection
327-
databases.remove(redisURI);
328-
database.getConnection().close();
329-
database.getCircuitBreaker().removeListener(this::onCircuitBreakerStateChange);
361+
doByExclusiveLock(() -> {
362+
RedisDatabase<C> database = null;
363+
database = databases.get(redisURI);
364+
if (database == null) {
365+
throw new IllegalArgumentException("Database not found: " + redisURI);
366+
}
367+
368+
if (current.getRedisURI().equals(redisURI)) {
369+
throw new UnsupportedOperationException("Cannot remove the currently active database: " + redisURI);
370+
}
371+
372+
// Remove the database and close its connection
373+
databases.remove(redisURI);
374+
database.close();
375+
});
330376
}
331377

332378
}

src/main/java/io/lettuce/core/failover/StatefulRedisMultiDbPubSubConnectionImpl.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,18 @@ public StatefulRedisMultiDbPubSubConnectionImpl(
4343

4444
@Override
4545
public void addListener(RedisPubSubListener<K, V> listener) {
46-
pubSubListeners.add(listener);
47-
current.getConnection().addListener(listener);
46+
doBySharedLock(() -> {
47+
pubSubListeners.add(listener);
48+
current.getConnection().addListener(listener);
49+
});
4850
}
4951

5052
@Override
5153
public void removeListener(RedisPubSubListener<K, V> listener) {
52-
pubSubListeners.remove(listener);
53-
current.getConnection().removeListener(listener);
54+
doBySharedLock(() -> {
55+
pubSubListeners.remove(listener);
56+
current.getConnection().removeListener(listener);
57+
});
5458
}
5559

5660
@Override
@@ -87,13 +91,15 @@ protected RedisPubSubReactiveCommandsImpl<K, V> newRedisReactiveCommandsImpl() {
8791
public void switchToDatabase(RedisURI redisURI) {
8892

8993
RedisDatabase<StatefulRedisPubSubConnection<K, V>> fromDb = current;
90-
super.switchToDatabase(redisURI);
91-
pubSubListeners.forEach(listener -> {
92-
current.getConnection().addListener(listener);
93-
fromDb.getConnection().removeListener(listener);
94+
doByExclusiveLock(() -> {
95+
super.switchToDatabase(redisURI);
96+
pubSubListeners.forEach(listener -> {
97+
current.getConnection().addListener(listener);
98+
fromDb.getConnection().removeListener(listener);
99+
});
100+
101+
moveSubscriptions(fromDb, current);
94102
});
95-
96-
moveSubscriptions(fromDb, current);
97103
}
98104

99105
@SuppressWarnings("unchecked")

0 commit comments

Comments
 (0)