Visitar URL original
Fail over to a recovered database when the active one is inoperable by ggivo · Pull Request #4765 · redis/jedis · GitHub
Skip to content

Fail over to a recovered database when the active one is inoperable - #4765

Merged
ggivo merged 3 commits into
masterfrom
multidb-lower-weight-recovery
Oct 2, 2026
Merged

ggivo merged 3 commits into
masterfrom
multidb-lower-weight-recovery

Conversation

@ggivo

@ggivo ggivo commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4764
Follow-up to #4755

What

Retries a failover that found no target, so MultiDbClient switches to a database that becomes usable later, whatever its weight, after JedisTemporaryNotAvailableException.

Why

#4756 keeps the health checks alive after a total outage. The remaining part of #4755 is that when the active database is unhealthy and no other one is usable, the failover is never retried. A lower-weight database turning healthy is ignored (failback only promotes higher weights), a candidate that was still in its grace period is never picked up because nothing announces the end of the grace period, and with health checks disabled there are no events at all. The client stays on the failing database until a manual setActiveDatabase().

Changes

  • MultiDbConnectionProvider: a single failoverIfActiveInoperable() switches away from an inoperable active database to the best operable one. When there is none, the failure counts as a failover attempt as before.
  • It runs from two places: when a non-active database turns healthy (health event), and from assertOperability() when a command cannot get a connection from an inoperable active database. A failed attempt is counted the same way in both.
  • The exhausted-attempts limit no longer prevents recovery; it only decides between the temporary and the permanent exception.
  • forceActiveDatabase() still pins the database, because the other databases are in grace for the pin duration.
  • Tests: userCommand_connectionExceptions_thenMultipleTemporary_thenPermanent_inOrder now expects the first failed command to fail over; the manual-switch test becomes ...failsOverAutomatically_healthCheckDriven; unit tests for health events counting as attempts, the command trigger after a grace period, and the forced pin.
  • docs/failover.md: describes the recovery after a failed failover.

Notes

  • The command that triggers the switch still fails; the following ones use the new database.
  • Pipelines and transactions do not call assertOperability(), so only the health event covers them.

Note

Medium Risk
Changes core MultiDb failover selection and recovery timing; mistakes could cause unexpected database switches or stuck routing, though behavior is heavily tested and forced pinning is preserved.

Overview
MultiDbClient now retries failover when the active database is unusable but no target was available earlier, so clients can recover automatically—even after JedisPermanentlyNotAvailableException—without manual setActiveDatabase().

MultiDbConnectionProvider adds failoverIfActiveInoperable(), which moves traffic to the best operable database regardless of weight. It runs when a non-active database becomes healthy (health listener) and from assertOperability() when a command cannot get a connection from the active DB (using tryLock so the failing command does not block). Failed attempts still count toward maxNumFailoverAttempts; exceeding the limit only chooses temporary vs permanent exceptions, it no longer blocks later recovery. forceActiveDatabase() pinning is unchanged (alternates stay in grace).

docs/failover.md documents automatic switch-back when any database becomes usable again and softens permanent-exception guidance to alert rather than shut down.

Tests now expect command-triggered failover to the secondary, automatic failover when only a lower-weight DB recovers after a total outage, plus unit coverage for health-driven attempt counting, post-grace assertOperability, and forced-active behavior.

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

When the active database turned unhealthy with no failover target, the
failover was never retried: a database that became usable later, e.g.
one with a lower weight than the active one, was ignored by both the
health event handler and failback, so commands kept failing until a
manual setActiveDatabase(), also after the failure turned permanent.

Retry the failover when another database turns healthy, and when a
command cannot get a connection from an inoperable active database,
which also covers a target whose grace period ended without an event
and configurations with health checks disabled. Both paths share one
implementation, so a failed attempt counts the same way for health
events and commands.

The test that relied on two consecutive raw failures with a usable
candidate now expects failover on the first failed command.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 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-10-02T06:37:21.220222Z 0faf241 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.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Test Results

  206 files  ±0    206 suites  ±0   7m 48s ⏱️ -6s
8 121 tests ±0  8 053 ✅ +237  68 💤  - 237  0 ❌ ±0 
8 141 runs  ±0  8 073 ✅ +237  68 💤  - 237  0 ❌ ±0 

Results for commit 0faf241. ± Comparison against base commit 5079fbd.

This pull request removes 1 and adds 1 tests. Note that renamed tests count towards both.
redis.clients.jedis.mcf.MultiDbConnectionProviderTest ‑ userCommand_afterPermanent_onlySecondaryHealthyAgain_requiresManualSwitch_healthCheckDriven
redis.clients.jedis.mcf.MultiDbConnectionProviderTest ‑ userCommand_afterPermanent_onlySecondaryHealthyAgain_failsOverAutomatically_healthCheckDriven

♻️ This comment has been updated with latest results.

@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: 35dbcc0234

ℹ️ 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 docs/failover.md Resolved

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 24ed568. Configure here.

private void failoverIfActiveInoperable() {
Database current = activeDatabase;
if (!current.isHealthy()) switchToHealthyDatabase(SwitchReason.HEALTH_CHECK, current);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Failover can skip the grace period

Medium Severity

failoverIfActiveInoperable() switches away from an inoperable active database without calling setGracePeriod(). Health status is published to isHealthy() before onHealthStatusChange runs, so a command (or a peer HEALTHY event) can move activeDatabase first. The later UNHEALTHY handler then sees the database is no longer active and skips grace entirely. That database is eligible again as soon as it reports healthy, which undermines the flapping protection grace is meant to provide.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 24ed568. Configure here.

A command that finds the active database inoperable while another one is
operable took activeDatabaseChangeLock, so commands queued behind a
switch, which can include a validation PING under that lock.

Commands now only try the lock and skip when a switch is in progress:
the command fails anyway and the next failing command re-evaluates the
state. The health event path is unchanged.

@atakavci atakavci left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@ggivo
ggivo merged commit c0c3c5d into master Oct 2, 2026
14 of 16 checks passed
ggivo added a commit that referenced this pull request Oct 5, 2026
…ctive one is inoperable (#4774)

Fail over to a recovered database when the active one is inoperable (#4765)

(cherry picked from commit c0c3c5d)

8.x adaptation: MultiDbProviderHealthStatusChangeTest mocks
TrackingConnectionPool (master gets this from #4668).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MultiDbClient does not fail over to a database that becomes usable after a failed failover

2 participants