Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
293e314
fix(csc): avoid draining checked-out cache connections
Grivn Oct 16, 2026
8758e68
fix(csc): preserve concurrent cache hits for busy owners
Grivn Oct 16, 2026
56d0728
Merge upstream master for cache response cloning
Grivn Oct 16, 2026
4cc62a1
fix(csc): initialize grace before publishing cache entries
Grivn Oct 16, 2026
b229c11
Merge master and preserve cache owner protection
Grivn Oct 27, 2026
53b86ce
Reduce cache checkout lock contention
Grivn Oct 27, 2026
1afc08d
Protect idle cached connections during maintenance
Grivn Nov 4, 2026
4236732
Fix cache ownership across maintenance and shutdown
Grivn Nov 4, 2026
4b7c518
fix(csc): coordinate maintenance close and interrupted checkout
Grivn Nov 4, 2026
b34798d
fix(csc): preserve shutdown for custom connection classes
Grivn Nov 4, 2026
185793e
fix(csc): streamline own hits and defer idle close callbacks
Grivn Nov 4, 2026
d75d36c
fix(csc): defer push listeners past idle owner drains
Grivn Nov 4, 2026
dcb8ceb
fix(csc): coordinate migration cleanup and reauthentication
Grivn Nov 5, 2026
e3de6a2
fix(csc): release callbacks outside the pool lock
Grivn Nov 5, 2026
2eb32d3
Merge master into fix/cache-busy-owner-invalidation
Grivn Nov 5, 2026
e93b9c8
fix(csc): finish removed-owner disconnect after draining
Grivn Nov 5, 2026
4a6640a
fix(csc): dispatch TTL completions outside handler locks
Grivn Nov 5, 2026
0e46407
fix(csc): preserve maintenance event order outside locks
Grivn Nov 5, 2026
5e30d88
fix(csc): recheck Sentinel ownership and order disconnect events
Grivn Nov 5, 2026
4fca5bd
fix(csc): defer topology refresh beyond idle owner drains
Grivn Nov 5, 2026
dce6482
fix(csc): release idle AUTH ownership before topology callbacks
Grivn Nov 6, 2026
3c40947
fix(csc): preserve maintenance callbacks across reconfiguration
Grivn Nov 6, 2026
408c694
fix(csc): serialize maintenance transitions with handoffs
Grivn Nov 6, 2026
666f78d
fix(csc): finish idle disconnects before connection reuse
Grivn Nov 6, 2026
85488e4
fix(csc): avoid maintenance release and finalizer deadlocks
Grivn Nov 6, 2026
94aafff
fix(pool): release maintenance lock before waiting for a slot
Grivn Nov 6, 2026
bb486d2
fix(csc): refresh reused owners and guard rejected releases
Grivn Nov 7, 2026
984fe9b
fix(csc): clear ownership when discarding cached connections
Grivn Nov 7, 2026
0beab63
fix(csc): keep idle invalidation polls nonblocking during maintenance
Grivn Nov 7, 2026
943f490
fix(csc): defer reentrant finalizer cleanup until idle drain completes
Grivn Nov 7, 2026
426b0f7
fix(csc): finish reentrant pool shutdown outside AUTH locking
Grivn Nov 7, 2026
7db6bd6
Fix cached pool teardown reentry and cancelled maintenance delivery
Grivn Nov 8, 2026
0b408fb
fix(csc): defer nested shutdown during final cached release
Grivn Nov 9, 2026
887b027
Merge current master and preserve cached-pool ownership ordering
Grivn Nov 9, 2026
342579e
fix: defer cached teardown during pool snapshot and rejected release
Grivn Nov 11, 2026
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
11 changes: 9 additions & 2 deletions redis/_parsers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ class PushNotificationsParser(Protocol):
node_moving_push_handler_func: Optional[Callable] = None
maintenance_push_handler_func: Optional[Callable] = None
oss_cluster_maint_push_handler_func: Optional[Callable] = None
# Socket owners can postpone topology work without replacing the registered
# handler, which a concurrent pool configuration update may change.
_deferred_oss_cluster_handler: Optional[Callable] = None

def handle_pubsub_push_response(self, response):
"""Handle pubsub push responses"""
Expand Down Expand Up @@ -432,8 +435,12 @@ def handle_push_response(self, response, **kwargs):
if notification is not None:
if self.maintenance_push_handler_func:
self.maintenance_push_handler_func(notification)
if self.oss_cluster_maint_push_handler_func:
self.oss_cluster_maint_push_handler_func(notification)
handler = self.oss_cluster_maint_push_handler_func
if handler:
if self._deferred_oss_cluster_handler is not None:
self._deferred_oss_cluster_handler(handler, notification)
else:
handler(notification)
except Exception as e:
logger.error(
"Error handling {} message ({}): {}".format(msg_type, response, e)
Expand Down
1 change: 1 addition & 0 deletions redis/asyncio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3417,6 +3417,7 @@ async def re_auth_callback(self, token: TokenInterface):
error, AuthenticationError
),
)
conn.set_re_auth_token(None)
except AuthenticationError:
raise
except Exception as e:
Expand Down
69 changes: 36 additions & 33 deletions redis/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,40 +369,43 @@ def delete_by_cache_keys(self, cache_keys: list[CacheKey]) -> list[bool]:
return response

def delete_by_redis_keys(self, redis_keys: list[bytes] | list[str]) -> list[bool]:
response = []
keys_to_delete = []

for redis_key in redis_keys:
# Prepare both versions for lookup
candidates = [redis_key]
if isinstance(redis_key, str):
candidates.append(redis_key.encode("utf-8"))
elif isinstance(redis_key, bytes):
try:
candidates.append(redis_key.decode("utf-8"))
except UnicodeDecodeError:
pass # Non-UTF-8 bytes, skip str version

# The reverse index answers this without walking the map. Both spellings are
# looked up because an entry is indexed under its keys exactly as the invocation
# supplied them, while the server names them in its own encoding. The two results
# are unioned, so an entry indexed under both spellings of this one key is
# collected once.
holders: set[CacheKey] = set()
for candidate in candidates:
holders |= self._cache.holders_of(candidate)

# An invalidation message never carries more than one key, so an entry holding
# several keys (MGET) cannot be collected twice by one call. A duplicate pop for
# a multi-key batch is not a reachable case - do not "fix" it.
for cache_key in holders:
keys_to_delete.append(cache_key)
response.append(True)

for key in keys_to_delete:
self._cache.pop(key)
# Independent owners can receive invalidations for the same Redis key.
# Keep the holder snapshots and their removal in one atomic operation.
with self._cache._lock:
response = []
keys_to_delete = []

for redis_key in redis_keys:
# Prepare both versions for lookup
candidates = [redis_key]
if isinstance(redis_key, str):
candidates.append(redis_key.encode("utf-8"))
elif isinstance(redis_key, bytes):
try:
candidates.append(redis_key.decode("utf-8"))
except UnicodeDecodeError:
pass # Non-UTF-8 bytes, skip str version

# The reverse index answers this without walking the map. Both spellings are
# looked up because an entry is indexed under its keys exactly as the invocation
# supplied them, while the server names them in its own encoding. The two results
# are unioned, so an entry indexed under both spellings of this one key is
# collected once.
holders: set[CacheKey] = set()
for candidate in candidates:
holders |= self._cache.holders_of(candidate)

# An invalidation message never carries more than one key, so an entry holding
# several keys (MGET) cannot be collected twice by one call. A duplicate pop for
# a multi-key batch is not a reachable case - do not "fix" it.
for cache_key in holders:
keys_to_delete.append(cache_key)
response.append(True)

for key in keys_to_delete:
self._cache.pop(key)

return response
return response

def flush(self) -> int:
elem_count = len(self._cache)
Expand Down
4 changes: 3 additions & 1 deletion redis/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
from redis.maint_notifications import (
MaintNotificationsConfig,
OSSMaintNotificationsHandler,
_defer_maintenance_events,
)
from redis.observability.recorder import (
record_error_count,
Expand Down Expand Up @@ -3044,7 +3045,7 @@ def _free_pool_connections(pool) -> None:
closed once their current command completes, and disconnect its free
connections.
"""
with pool._lock:
with _defer_maintenance_events(), pool._lock:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer cached teardown beyond the outer topology lock

With client-side caching, topology cleanup can still deadlock if GC finalizes an abandoned Redis.from_pool(pool) client while disconnecting a free socket and a peer is draining another idle owner. disconnect_free_connections() defers cached teardown only within its own scope; on exit, it joins the owner's _in_use_lock while this enclosing pool._lock is still held. The peer needs that pool lock to finish timeout restoration, so neither operation can complete, even with maintenance notifications disabled. A fresh automatic-GC/socket-pair probe on Python 3.13 hangs in _defer_cached_disconnects().__exit__ here and completes on the parent; unlike the earlier completion-listener issue, no event listener is involved. Extend cached-disconnect deferral around the outer topology lock as well.

Useful? React with 👍 / 👎.

pool.update_active_connections_for_reconnect()
pool.disconnect_free_connections()

Expand Down Expand Up @@ -3425,6 +3426,7 @@ def initialize(
additional_startup_nodes_info = []

with (
_defer_maintenance_events(),
self._initialization_lock,
self._initializing_on_this_thread(),
self._counting_failed_refresh(),
Expand Down
Loading