Skip to content

SOLR-7394: Clear recovering shard-terms entry when recovery is abandoned - #5003

Open
nick-boss-tech wants to merge 11 commits into
apache:mainfrom
nick-boss-tech:solr-7394-recovery
Open

nick-boss-tech wants to merge 11 commits into
apache:mainfrom
nick-boss-tech:solr-7394-recovery

Conversation

@nick-boss-tech

@nick-boss-tech nick-boss-tech commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖 (posted on behalf of Nick Shanin)

https://issues.apache.org/jira/browse/SOLR-7394

What happens today

When a leader-eligible replica exhausts its recovery retries, RecoveryStrategy publishes RECOVERY_FAILED but leaves the replica's _recovering entry in the shard terms in place. That entry permanently excludes the replica from leader election, even though no recovery is running anymore: the replica is stuck out of the election for good, not just until it catches up.

What this change does

On the retry-exhaustion path, RecoveryStrategy now clears the replica's _recovering shard-terms entry and writes back the term the replica had before recovery began (the value startRecovering saved under that marker) before publishing RECOVERY_FAILED. The failed replica cannot outrank a replica that stayed current, since its restored term is the pre-recovery one, and election among failed replicas keeps their pre-recovery ordering. The shard-terms cleanup no longer shares a try block with the terminal-state publication: if the cleanup itself throws, the failure is logged and RECOVERY_FAILED is still published, with close() in finally. The change adds two test-only seams for deterministic coverage, TestInjection.failRecovery and TestInjection.recoveryMaxRetriesOverride, both cleared by TestInjection.reset().

Proof

Verified at head 9857d9e on 2026-10-06 (changelog YAML parse clean, tidy clean, Error Prone compile clean, :solr:core:check -x test green).

  • ZkShardTermsRecoveryFailureTest (1/1) is a cloud-level test: it exhausts retries through the seams, asserts the _recovering entry is gone, the term is restored to the pre-recovery value (0 for this brand-new replica), and the replica can win an election once the higher-term replicas are gone. It fails on the unpatched base with "failed replica must not retain a _recovering shard-terms entry".
  • RecoveryStrategyTest (1/1) forces the shard-terms cleanup to throw and verifies RECOVERY_FAILED is still published and the listener still notified; it fails against the previous shared-try structure.
  • ShardTermsTest (6/6) covers the term restore directly, including two failed replicas with different saved terms, where only the fresher one stays leader-eligible. Run against the reset-to-0 code this change replaces, exactly the three recoveryFailed tests in the class fail.
  • TestTestInjection (5/5) pins the hook contract and the reset coverage for both seams.

A choice to check

If the shard-terms cleanup fails, this change logs the failure and publishes the terminal state anyway, on the reasoning that a replica stuck showing RECOVERING with no recovery running is worse than a stale marker on a terminal replica. The alternative is to let a cleanup failure mask the RECOVERY_FAILED publication. Was publishing anyway the right call?

A second call in this change: on failure, the term startRecovering saved under the _recovering marker is written back, rather than being discarded and reset to 0. The saved term is a lower bound on the replica's data: a failed recovery never rolls back the index (peer sync only adds data, and a replication fetch replaces the index only when it succeeds), so the replica still holds at least the updates its saved term reflects. Restoring keeps the pre-recovery ordering among failed replicas; resetting to 0 would tie two failed replicas and let election order pick the staler one, and the fresher replica's extra updates would then be discarded when it syncs from that leader. The cost of restoring: a replica whose index was damaged by whatever made recovery fail still carries its old term, and term comparisons cannot see index damage. Was restoring the saved term the right call?

Limits

The trade behind the restore: once the higher-term replicas stop participating, a replica whose recovery never completed can be elected leader, and replicas that return afterwards then sync to it, so the shard can end up led by a replica with older data rather than having no leader at all. The restored term keeps a failed replica behind every replica that stayed current, so this only happens when no healthier replica remains. Retry exhaustion is reached through the test-only seams; a naturally timed exhaustion after real retries in a live cluster is not exercised. Election ordering after the restore is covered for two failed replicas in ShardTermsTest; a live election race among several recovering replicas at mixed terms is not covered.

Changelog: changelog/unreleased/SOLR-7394.yml

AI assistance

AI agents assisted with research, implementation, review, and drafting. Nick Shanin directed the work and takes responsibility for this contribution.

- RecoveryStrategy.testing_maxRetriesOverride: allows tests to exhaust
  recovery retries quickly (follows the in-file precedent of
  testing_beforeReplayBufferingUpdates).
- TestInjection.failRecovery + hook in RecoveryStrategy: allows tests to
  force recovery failure deterministically after RECOVERING is published.
…ng shard terms

ZkShardTermsRecoveryFailureTest verifies that when a recovering replica
exhausts its recovery attempts, RecoveryStrategy clears the replica's
_recovering shard-terms entry and resets its term to 0, restoring its
eligibility to win a future leader election.
If clearing the failed recovery state from shard terms throws, the
terminal state publication must still happen. A replica stuck showing
RECOVERING with no recovery running is worse than a stale shard-term
marker on a replica that has reached RECOVERY_FAILED. The cleanup
failure is logged, not hidden.

Also remove the branch-local testing handoff; it must not ship upstream.
recoveryFailed ran the shard terms cleanup and the
RECOVERY_FAILED publication in the same try block, so a
failure in the cleanup skipped the publication and left the
replica without its terminal state, the opposite of the
failure policy this change states. The cleanup failure is
now caught and logged locally, the publication always runs,
and close() stays in finally. RecoveryStrategyTest forces
the cleanup to throw and verifies the terminal state is
still published and the listener notified.
recoveryFailed wrote term 0, discarding the term that startRecovering saved
for leader election. A failed recovery never rolls back the replica index,
so the saved term is a sound lower bound on the replica data; writing 0 made
a replica one write behind indistinguishable from a brand-new empty replica,
and two failed replicas tied at 0 let election order pick the staler one.
The saved term is now written back, and ShardTermsTest covers the case of
two failed replicas with different saved terms.

Also from the re-review: injectFailRecovery follows the TestInjection
convention (the hook throws when armed and is called under assert, with the
true:100 arming form), the retry-limit override moves from RecoveryStrategy
into TestInjection so TestInjection.reset() clears it, RecoveryStrategyTest
calls the now package-private recoveryFailed directly instead of through
reflection, and the cloud test configures its cluster in @BeforeClass.
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.

1 participant