RedisCluster: Per-node health tracking and non-blocking initialize()#4083
RedisCluster: Per-node health tracking and non-blocking initialize()#4083ngabhanenetskope wants to merge 1 commit into
Conversation
…luster Two improvements to RedisCluster's resilience during node failures: 1. Per-node health tracking (NodeHealthManager): After N consecutive TimeoutError/ConnectionError on a node (default: 5), the node is marked unavailable. Subsequent requests to that node fail immediately with ConnectionError instead of blocking for socket_timeout seconds. After recovery_time (default: 10s), one probe request is allowed through (half-open pattern). On success, the node is marked available again. This prevents the initial pool saturation cascade: instead of 50 greenlets all blocking on a dead node for 5s each, the 6th+ greenlet fails instantly once the threshold is crossed. 2. Non-blocking initialize(): Changed from blocking lock acquisition to try-lock pattern. If another thread is already refreshing the cluster topology, additional threads return immediately instead of queuing on the lock. They use the stale (but functional for healthy nodes) topology until the refresh completes. This eliminates the thundering herd on initialize(): previously 50 threads timing out simultaneously would all queue on _initialization_lock, serializing 50 redundant CLUSTER SLOTS calls. Now only the first thread refreshes; others continue serving requests to healthy nodes. Configuration (RedisCluster constructor): - node_health_failure_threshold: int = 5 - node_health_recovery_time: float = 10.0 Fixes redis#4074
|
Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset. In case there are security findings, they will be communicated to you as a comment inside the PR. Hope you’ll enjoy using Jit. Questions? Comments? Want to learn more? Get in touch with us. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Reviewed by Cursor Bugbot for commit ec068bd. Configure here.
| raise ConnectionError( | ||
| f"Node {target_node.name} is marked unavailable " | ||
| f"(consecutive failures exceeded threshold)" | ||
| ) |
There was a problem hiding this comment.
Health check error triggers unnecessary pool disconnect and reinitialize
Medium Severity
The ConnectionError raised by the health check is caught by the except (ConnectionError, TimeoutError) handler below, which then executes pool disconnection under pool._lock, calls nodes_manager.initialize(), and calls move_node_to_end_of_cached_nodes — all unnecessary for a synthetic fast-fail error. Additionally, ConnectionError is in ERRORS_ALLOW_RETRY, so the outer _internal_execute_command loop retries the same unavailable node multiple times, each time triggering pool manipulation and topology refresh. Under 250 greenlets with 3 retries, this causes ~1000 pool._lock acquisitions and initialize() calls per unavailable node, significantly undermining the "fail fast in 0ms" design goal.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit ec068bd. Configure here.
| # Allow a probe request through (half-open) | ||
| del self._node_unavailable_since[node_name] | ||
| self._node_failures[node_name] = self.failure_threshold - 1 | ||
| return True |
There was a problem hiding this comment.
Half-open state allows all requests, not single probe
Medium Severity
The half-open transition in is_available deletes the node from _node_unavailable_since and returns True. After this first call, every subsequent concurrent call also returns True (the node is no longer in the dict), allowing all requests through simultaneously — not just one probe as documented and intended. Under 250 greenlets, this means the potentially-still-dead node gets flooded after recovery_time, causing the same pool saturation the circuit breaker aims to prevent.
Reviewed by Cursor Bugbot for commit ec068bd. Configure here.


Summary
Adds two mechanisms to improve RedisCluster resilience during transient node failures under high concurrency:
Per-node health tracking (
NodeHealthManager) — After N consecutiveTimeoutError/ConnectionErroron a node (default: 5), subsequent requests to that node fail immediately instead of blocking forsocket_timeout. Afterrecovery_time(default: 10s), one probe request is allowed through (half-open pattern). On success, the node is marked available again.Non-blocking
initialize()— Changed from blocking lock acquisition to try-lock. If another thread is already refreshing topology, additional threads return immediately using stale (but functional for healthy nodes) topology instead of queuing on_initialization_lock.Motivation
Under high concurrency (e.g., gevent with 250 greenlets,
max_connections_per_node=50,socket_timeout=5s), a single node failure causes:initialize()(thundering herd)_initialization_lock, serializing 50 redundant CLUSTER SLOTS callsWith this fix:
See #4074 for full analysis.
Configuration
Behavior
Tests
TestNodeHealthManager— 7 tests covering threshold, recovery, per-node isolation, and cluster integrationRelated