Repository: redis/redis-py
Pull request #3514 (2025-02-18 13:11:58)

Remove decreasing of created connections count when releasing not owned by connection pool connection(fixes issue #2832).
### Pull Request check-list

_Please make sure to review and check all of these items:_

- [ ] Do tests and lints pass with this change?
- [ ] Do the CI tests pass with this change (enable it first in your forked repo and wait for the github action build to finish)?
- [ ] Is the new or changed code fully tested?
- [ ] Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?
- [ ] Is there an example added to the examples folder (if applicable)?
- [ ] Was the change added to CHANGES file?

_NOTE: these things are not required to open a PR and can be done
afterwards / while the PR is open._

### Description of change

* Removing decreasing of created connections count when releasing not owned by connection pool connection([issue #2832](https://github.com/redis/redis-py/issues/2832)).
* Fixed another issue that was allowing adding connections to a pool owned by other pools.
* Adding unit tests.


Patch:
diff --git a/redis/connection.py b/redis/connection.py
index d59a9b06..ece17d75 100644
--- a/redis/connection.py
+++ b/redis/connection.py
@@ -1532,7 +1532,7 @@ class ConnectionPool:
             except KeyError:
                 # Gracefully fail when a connection is returned to this pool
                 # that the pool doesn't actually own
-                pass
+                return
 
             if self.owns_connection(connection):
                 self._available_connections.append(connection)
@@ -1540,10 +1540,10 @@ class ConnectionPool:
                     AfterConnectionReleasedEvent(connection)
                 )
             else:
-                # pool doesn't own this connection. do not add it back
-                # to the pool and decrement the count so that another
-                # connection can take its place if needed
-                self._created_connections -= 1
+                # Pool doesn't own this connection, do not add it back
+                # to the pool.
+                # The created connections count should not be changed,
+                # because the connection was not created by the pool.
                 connection.disconnect()
                 return
 


Test patch:
diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py
index 387a0f45..65f42923 100644
--- a/tests/test_connection_pool.py
+++ b/tests/test_connection_pool.py
@@ -91,6 +91,21 @@ class TestConnectionPool:
         c2 = pool.get_connection()
         assert c1 == c2
 
+    def test_release_not_owned_connection(self, master_host):
+        connection_kwargs = {"host": master_host[0], "port": master_host[1]}
+        pool1 = self.get_pool(connection_kwargs=connection_kwargs)
+        c1 = pool1.get_connection("_")
+        pool2 = self.get_pool(
+            connection_kwargs={"host": master_host[0], "port": master_host[1]}
+        )
+        c2 = pool2.get_connection("_")
+        pool2.release(c2)
+
+        assert len(pool2._available_connections) == 1
+
+        pool2.release(c1)
+        assert len(pool2._available_connections) == 1
+
     def test_repr_contains_db_info_tcp(self):
         connection_kwargs = {
             "host": "localhost",
diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py
index 0e8e8958..8b9e9fb9 100644
--- a/tests/test_multiprocessing.py
+++ b/tests/test_multiprocessing.py
@@ -84,6 +84,40 @@ class TestMultiprocessing:
         proc.join(3)
         assert proc.exitcode == 0
 
+    @pytest.mark.parametrize("max_connections", [2, None])
+    def test_release_parent_connection_from_pool_in_child_process(
+        self, max_connections, master_host
+    ):
+        """
+        A connection owned by a parent should not decrease the _created_connections
+        counter in child when released - when the child process starts to use the
+        pool it resets all the counters that have been set in the parent process.
+        """
+
+        pool = ConnectionPool.from_url(
+            f"redis://{master_host[0]}:{master_host[1]}",
+            max_connections=max_connections,
+        )
+
+        parent_conn = pool.get_connection("ping")
+
+        def target(pool, parent_conn):
+            with exit_callback(pool.disconnect):
+                child_conn = pool.get_connection("ping")
+                assert child_conn.pid != parent_conn.pid
+                pool.release(child_conn)
+                assert pool._created_connections == 1
+                assert child_conn in pool._available_connections
+                pool.release(parent_conn)
+                assert pool._created_connections == 1
+                assert child_conn in pool._available_connections
+                assert parent_conn not in pool._available_connections
+
+        proc = multiprocessing.Process(target=target, args=(pool, parent_conn))
+        proc.start()
+        proc.join(3)
+        assert proc.exitcode == 0
+
     @pytest.mark.parametrize("max_connections", [1, 2, None])
     def test_pool(self, max_connections, master_host):
         """