-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Fix redundant disposal in LettucePoolingConnectionProvider by implementing SmartLifecycle #3100 #3164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
See spring-projects#3103 Signed-off-by: UHyeon Jeong <[email protected]>
@@ -979,6 +980,9 @@ public void stop() { | |||
dispose(reactiveConnectionProvider); | |||
reactiveConnectionProvider = null; | |||
|
|||
dispose(clusterCommandExecutor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In these changes, ClusterCommandExecutor
is being disposed after disposing the connection provider and so there's no behavioral change. Shouldn't clusterCommandExecutor
be disposed (and thus its shared connection returned to the pool) before disposing connectionProvider
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You’re right that there’s no behavioral change in ClusterCommandExecutor itself. However, despite other resources at LettuceConnectionFactory are disposed at stop
, only ClusterCommandExecutor
disposed at destroy
method. I was concerned that if we leave the dispose call in destroy, it could be executed twice in the future, as LettucePoolingConnectionProvider
did. Since the stop method uses an AtomicReference guard, this change ensures safe and idempotent disposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I believe the order of disposing connectionProvider doesn’t affect behavior in this case.
Below are the previous implementations of LettucePoolingConnectionProvider#destroy, #release As you can see, this method fully clears all pools and references on the first invocation. If it were called again, it could potentially throw exceptions or behave unexpectedly due to already-released resources.
@Override
public void destroy() throws Exception {
List<CompletableFuture<?>> futures = new ArrayList<>();
if (!poolRef.isEmpty() || !asyncPoolRef.isEmpty()) {
log.warn("LettucePoolingConnectionProvider contains unreleased connections");
}
if (!inProgressAsyncPoolRef.isEmpty()) {
log.warn("LettucePoolingConnectionProvider has active connection retrievals");
inProgressAsyncPoolRef.forEach((k, v) -> futures.add(k.thenApply(StatefulConnection::closeAsync)));
}
if (!poolRef.isEmpty()) {
poolRef.forEach((connection, pool) -> pool.returnObject(connection));
poolRef.clear();
}
if (!asyncPoolRef.isEmpty()) {
asyncPoolRef.forEach((connection, pool) -> futures.add(pool.release(connection)));
asyncPoolRef.clear();
}
pools.forEach((type, pool) -> pool.close());
CompletableFuture
.allOf(futures.stream().map(it -> it.exceptionally(LettuceFutureUtils.ignoreErrors()))
.toArray(CompletableFuture[]::new)) //
.thenCompose(ignored -> {
CompletableFuture[] poolClose = asyncPools.values().stream().map(AsyncPool::closeAsync)
.map(it -> it.exceptionally(LettuceFutureUtils.ignoreErrors())).toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(poolClose);
}) //
.thenRun(() -> {
asyncPoolRef.clear();
inProgressAsyncPoolRef.clear();
}) //
.join();
pools.clear();
}
@Override
public void release(StatefulConnection<?, ?> connection) {
GenericObjectPool<StatefulConnection<?, ?>> pool = poolRef.remove(connection);
if (pool == null) {
AsyncPool<StatefulConnection<?, ?>> asyncPool = asyncPoolRef.remove(connection);
if (asyncPool == null) {
throw new PoolException("Returned connection " + connection
+ " was either previously returned or does not belong to this connection provider");
}
discardIfNecessary(connection);
asyncPool.release(connection).join();
return;
}
discardIfNecessary(connection);
pool.returnObject(connection);
}
To prevent this, the disposal logic was moved to under the state safe guard, ensuring it’s executed only once and making the lifecycle handling more robust.
Enhancements to
LettuceConnectionFactory
:ClusterCommandExecutor
during thestop
method to ensure resources are released cleanly.ClusterCommandExecutor
from thedestroy
method, as it is now handled in thestop
method.dispose
method to encapsulate the cleanup logic forClusterCommandExecutor
, improving code readability and maintainability.Enhancements to
LettucePoolingConnectionProvider
:SmartLifecycle
to manage the lifecycle of the connection provider, includingstart
,stop
, andisRunning
methods.AtomicReference
-basedState
enum to track the lifecycle state of the connection provider, ensuring thread-safe transitions between states.These updates improve the robustness and maintainability of the connection handling logic in the Spring Data Redis project.
I have found out that #3100 was reproduced when redis run with cluster mode.
LettuceConnectionFactory uses a connectionProvider that refered at ClusterCommandExecuter. When redis run with Cluster mode, it uses LettucePoolingConnectionProvider, which only implements DisposableBean.
When the LettuceConnectionFactory disposed, it disposes ConnectionProvider first, and disposes ClusterCommandExecuter next.
And when the ClusterCommandExecuter disposed, it disposes the connectionProvider - shared with LettuceConnectionFactory. As a result, LettucePoolingConnectionProvider's destroy method called twice.
Finally, the LettucePoolingConnectionProvider tried releasing connections in it's pool - that has 0 connections.
To solve this issue, I've implemented SmartLifeCycle at LettucePoolingConnectionProvider, and prevent duplicated stop method execution by AtomicReference based state management.
Solved #3100, See #3103