Skip to content

Add latency-based Redis Cluster read load balancing - #4323

Open
CaptainDredge wants to merge 2 commits into
redis:masterfrom
CaptainDredge:codex/latency-based-cluster-load-balancing
Open

CaptainDredge wants to merge 2 commits into
redis:masterfrom
CaptainDredge:codex/latency-based-cluster-load-balancing

Conversation

@CaptainDredge

@CaptainDredge CaptainDredge commented Sep 12, 2026

Copy link
Copy Markdown

Description of change

This pull request resolves #4185 by adding an opt-in LoadBalancingStrategy.LATENCY_BASED for Redis Cluster reads, with matching synchronous and asynchronous behavior. For each read, it samples two eligible nodes from the slot's primary and replicas and chooses the lower client-observed latency-and-load score:

aged_peak_ewma * (in_flight_requests + 1)

The implementation keeps per-client node state, records latency only after successful replica-safe reads, and always releases in-flight accounting on success, errors, redirects, timeouts, or cancellation. Writes contribute only to in-flight load, so a busy primary becomes less attractive without mixing write latency into the read estimator. Pipelines and transactions contribute in-flight load but do not train latency because batch durations are not comparable. Topology refresh preserves surviving node history, prunes departed nodes, and uses generation-tagged attempt tokens so stale completions cannot corrupt replacement-node state. Latency eligibility reuses the command metadata resolver, including registered custom reads, rather than maintaining a second static read-command list.

Assumptions and tradeoffs

  • The strategy is opt-in and includes the primary plus replicas, matching the scope of RANDOM and ROUND_ROBIN; replica staleness semantics are unchanged.
  • Measurements are local to each client. No cross-process coordination or server-side signal is assumed.
  • Only successful reads train latency. Existing retry and topology handling own hard failures instead of translating them into synthetic latency penalties.
  • Timing intentionally includes pool wait, successful reconnect, network, server work, parsing, and response callbacks because these are the delays observed by this client.
  • EWMA alpha is 0.2, baseline alpha is 0.05, the decay period is 10s, evidence expires after 30s, and cold nodes start at a 1ms baseline. These remain internal constants to avoid expanding the public API before operational evidence justifies tuning knobs.
  • The baseline is traffic-weighted. Expiring old evidence creates a small score discontinuity, but guarantees recovered-node exploration instead of possible permanent starvation.
  • In-flight accounting begins immediately after routing rather than making node selection and reservation one atomic API, avoiding a broad routing return-type change.

The change includes the implementation specification, sync/async unit and integration coverage, user documentation, and a reusable benchmark that externally degrades a Redis replica with CLIENT PAUSE rather than injecting delay inside redis-py.

Benchmark results

Two eligible nodes (one primary and one replica):

  • Degraded-node traffic: 0.01%
  • Degraded-phase p99: latency-based 0.626 ms; round-robin 6,056.736 ms
  • Recovered-node re-entry: 29.862s
  • Errors: 0

Three eligible nodes (one primary and two replicas):

  • Healthy traffic share: 32.29% / 36.57% / 31.14%
  • Degraded-replica traffic: 0.01%
  • Traffic redistributed across healthy nodes: 51.24% / 48.75%
  • Degraded-phase p99: latency-based 0.794 ms; round-robin 20,043.952 ms
  • Recovered-node re-entry: 29.838s
  • Errors: 0

Test coverage

  • Focused latency tracking and metadata regression tests: 125 passed
  • Fixed-client suite: 1,635 passed, 2 skipped
  • Standalone suite: 3,584 passed, 674 skipped, 26 xpassed
  • Cluster suite: 2,216 passed, 1,089 skipped, 2 xpassed
  • invoke all-tests: passed, including Ruff lint/format and Vulture
  • invoke build-docs: passed (existing documentation warnings remain)
  • git diff --check: passed

Pull Request check-list

  • Do tests and lints pass with this change?
  • Do the CI tests pass with this change (CI will run on this PR)?
  • Is the new or changed code fully tested?
  • Is a documentation update included?
  • Is there an example added where applicable? The cluster documentation includes opt-in usage.

Resolves #4185.


Note

Medium Risk
Changes cluster read routing and command lifecycle in sync/async clients (including pipelines/transactions), but the strategy is opt-in and existing strategies are unchanged; replica staleness and per-client measurement semantics still apply.

Overview
Adds an opt-in LoadBalancingStrategy.LATENCY_BASED for Redis Cluster reads. For each eligible read, the client samples two nodes (primary + replicas), scores them with decaying peak EWMA × in-flight load, and routes to the lower score. Successful replica-safe reads update latency; writes, failures, ASK redirects, and pipeline/transaction batches only affect in-flight accounting. Sync and async _execute_command, slot/keyless routing, topology reconcile, and pipeline/transaction paths are wired to start_request / finish_request.

Docs (clustering.rst), an internal spec, a local cluster_latency_load_balancing benchmark (external replica degradation via CLIENT PAUSE), and broad unit/integration tests cover the new strategy without changing existing round-robin/random behavior.

Reviewed by Cursor Bugbot for commit df85013. Bugbot is set up for automated code reviews on this repo. Configure here.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T00:39:55.505443Z df85013 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6252dad316

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread redis/cluster.py
Comment thread redis/cluster.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df8501300b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread redis/cluster.py
Comment on lines +5479 to +5483
latency_attempts = {
node_name: self._nodes_manager.read_load_balancer.start_request(
node_name
)
for node_name in nodes

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 Start sync pipeline tracking before connection acquisition

When a synchronous pipeline waits for an available connection or performs a slow HIMPORT preflight, these attempts are started only after get_connection() and _himport_prepare_pipeline() have completed, so concurrent reads see no in-flight load and can continue selecting the already-congested node. The async path starts its attempt before ClusterNode.execute_pipeline(), which performs connection acquisition internally, so move the sync start before connection acquisition as well to preserve load accounting and sync/async behavior.

AGENTS.md reference: AGENTS.md:L146-L149

Useful? React with 👍 / 👎.

Comment thread redis/cluster.py
getattr(self, "load_balancing_strategy", None)
== LoadBalancingStrategy.LATENCY_BASED
)
latency_sampling = latency_balancing and self._is_replica_safe(command)

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 Exclude blocking reads from latency history

When a replica-safe blocking read such as XREAD BLOCK completes normally, this condition enables sampling and records the caller-requested blocking interval as node latency. A successful timeout or delayed stream entry can therefore add seconds to the node's peak estimate and divert unrelated reads from that healthy node for the evidence TTL; blocking commands should retain in-flight accounting but not update historical latency.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClusterClient: latency-aware read load balancing (power-of-two-choices + peak-EWMA)

1 participant