Promote lowest-version replica on doc-rep failover and make node_version decider Lucene-aware - #22668
Conversation
… decider Lucene-aware During a rolling upgrade with document replication, primary failover promoted the highest-version in-sync replica (activeReplicaWithHighestVersion), pushing the primary to the newest node version. NodeVersionAllocationDecider then refuses to allocate that shard's replicas onto any not-yet-upgraded node, since it requires replica.version >= primary.version. Reproduced live in opensearch-project#22520: a .plugins-ml-config replica stuck UNASSIGNED during a 2.19.0 -> 2.19.4 patch upgrade, where both versions share the same Lucene minor (9.12.x) with no real segment-format incompatibility. Two changes: 1. RoutingNodes#unassignPrimaryAndPromoteActiveReplicaIfExists now promotes the lowest-version in-sync replica for document replication too, matching what segment replication already does (opensearch-project#9536). This keeps the primary at the minimum version present so it stays a valid allocation target for every node during the upgrade. The original promote-highest rule (#25277) existed only because sequence numbers were new and older nodes didn't understand them; that hazard no longer exists in any supported version skew. activeReplicaWithHighestVersion is now unused and removed. 2. NodeVersionAllocationDecider now additionally allows allocation/relocation when the target and source nodes' actual Lucene versions (Version#luceneVersion) are format-compatible (same-or-newer Lucene major.minor), not just when the raw OpenSearch version id is equal-or-newer. This stops OpenSearch patch upgrades that don't change the Lucene format (the common case) from being treated as incompatible, while still blocking genuine Lucene minor/major gaps. Fixes opensearch-project#22520 Signed-off-by: Ziwen Wan <wanzwnju@gmail.com>
438126b to
a2e24ab
Compare
PR Reviewer Guide 🔍(Review updated until commit a8cbcd6)Here are some key observations to aid the review process:
|
|
Persistent review updated to latest commit a2e24ab |
PR Code Suggestions ✨Latest suggestions up to a8cbcd6 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit a8cbcd6
Suggestions up to commit a229718
Suggestions up to commit 190a3f8
Suggestions up to commit 678509d
Suggestions up to commit d611d39
|
|
❌ Gradle check result for a2e24ab: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
RoutingNodes is annotated @publicapi(since = "1.0.0"), so removing the public activeReplicaWithHighestVersion method is a binary-incompatible change and fails the :server:japicmp check against the 3.7.0 baseline. Restore the method with its original signature and mark it @deprecated / @deprecatedapi(since = "3.8.0", forRemoval = "4.0.0") instead. It is no longer called from production code -- both replication types now promote via activeReplicaWithOldestVersion -- but it stays on the public surface until the next major. Signed-off-by: Ziwen Wan <wanzwnju@gmail.com>
|
Persistent review updated to latest commit a2e24ab |
|
❌ Gradle check result for a2e24ab: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit d611d39 |
|
❌ Gradle check result for d611d39: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
The assertThat message concatenation fits within the 140-column limit, so palantir-java-format keeps it on a single line. Matches ./gradlew spotlessApply. Signed-off-by: Ziwen Wan <wanzwnju@gmail.com>
|
Persistent review updated to latest commit 678509d |
|
❌ Gradle check result for 678509d: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
The segment-replication check in canAllocate compared Lucene versions in the wrong direction: it asked whether the candidate primary node could read the replica's segments. With segment replication the replica continuously reads segments written by the primary, so the question is the reverse -- whether the replica can read what the primary would write. As written the guard only fired when a newer OpenSearch version carried an older Lucene, which effectively disabled it and let a primary relocate next to a replica that could not read its segments. Caught by testRebalanceDoesNotAllocatePrimaryOnHigherVersionNodesSegrepEnabled. Swap the arguments so the replica is the reader, matching the direction already used by isVersionCompatibleAllocatingReplica, and add coverage for both the allowed (same Lucene minor, different OpenSearch patch) and blocked (newer Lucene minor) cases. Signed-off-by: Ziwen Wan <wanzwnju@gmail.com>
|
Persistent review updated to latest commit 190a3f8 |
|
❌ Gradle check result for 190a3f8: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
The IndexMetadata builder chain fits within the 140-column limit (139 chars), so palantir-java-format keeps it on a single line. Signed-off-by: Ziwen Wan <wanzwnju@gmail.com>
|
Persistent review updated to latest commit a229718 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #22668 +/- ##
============================================
+ Coverage 71.49% 71.51% +0.02%
+ Complexity 77005 77002 -3
============================================
Files 6156 6156
Lines 358413 358418 +5
Branches 52243 52247 +4
============================================
+ Hits 256237 256328 +91
+ Misses 81769 81713 -56
+ Partials 20407 20377 -30 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The Lucene-compatibility condition was added to four call sites but only two were exercised by tests, leaving isVersionCompatibleRelocatePrimary and the snapshot isVersionCompatible with uncovered branches. Add positive and negative cases for both: same Lucene major.minor across an OpenSearch patch difference (V_2_19_4 / V_2_19_0, Lucene 9.12.x) must be allowed, and a differing Lucene minor (V_2_19_0 / V_2_17_2, Lucene 9.12 vs 9.11) must stay blocked. Signed-off-by: Ziwen Wan <wanzwnju@gmail.com>
|
Persistent review updated to latest commit a8cbcd6 |
|
❌ Gradle check result for a8cbcd6: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit a8cbcd6 |
Description
During a rolling upgrade with document replication, a shard's replicas can become permanently unassignable even when there is no real segment-format incompatibility between the nodes involved. This PR removes two constraints that are stricter than Lucene actually requires. Details and discussion in #22520.
1. Document-replication failover promotes the highest-version replica.
RoutingNodes#unassignPrimaryAndPromoteActiveReplicaIfExistsbranches on replication type and callsactiveReplicaWithHighestVersionfor document replication, so each failover ratchets the primary onto the newest nodeversion present. But
NodeVersionAllocationDeciderenforcesreplica.version >= primary.version-- so once the primary has been promoted onto an upgraded node, that shard's replicas are refused on every not-yet-upgraded node. Promote-highest directly contradicts the invariant the decider is trying to maintain.Segment replication already promotes the oldest version (
activeReplicaWithOldestVersion, added in #9536). The promote-highest rule dates to elastic/elasticsearch#25277 (2017) and existed only because sequence numbers were then new and older nodes could not interpret them. That hazard does not exist in any currently supported version skew, so both replication types can share the same rule.Note this promotion runs inside
failShardand is therefore not gated byEnableAllocationDecider-- settingcluster.routing.allocation.enable: primariesduring an upgrade does not prevent it.2. The decider compares OpenSearch version ids, not Lucene versions.
The constraint the decider exists to enforce is a Lucene segment-format constraint -- its own javadoc says so, and the failure it prevents is
IndexFormatTooNewException. But it comparesVersionids. An OpenSearch patch upgrade that changes no Lucene minor is therefore treated as incompatible even though the on-disk format is byte-for-byte the same generation.Version#luceneVersionis already available onVersion, so comparing it is a small change.Real-world impact
Reproduced on a production cluster during a 2.19.0 -> 2.19.4 patch upgrade: a
.plugins-ml-configreplica stuck UNASSIGNED with_cluster/allocation/explaincitingnode_version, with the primary on 2.19.4 and the candidate node on 2.19.0. Both run Lucene 9.12.x, so the block is a false positive -- there is no format difference to protect against.Changes
RoutingNodes-- promote the lowest-version in-sync replica for both replication types;activeReplicaWithHighestVersionis now unused and removed.NodeVersionAllocationDecider-- add a Lucene-compatibility check:OR'd with the existing
onOrAftercheck inisVersionCompatibleAllocatingReplica,isVersionCompatibleRelocatePrimary,the snapshot-restore
isVersionCompatible, and the segment-replication block incanAllocate. Decision messages were updated so_cluster/allocation/explainstill explains which condition applied.A Lucene minor bump changes the codec/segment format; a Lucene patch bump does not. Keying on
major.minoris therefore the correct granularity -- this is not equivalent to "ignore the OpenSearch patch version." OpenSearch patch releases can and do bump the Lucene patch (2.19.2/3/4 -> 9.12.2/9.12.3), but never the Lucene minor. Genuine Lucene minor and major gaps remain blocked.Related Issues
Fixes #22520
Check List
FailedShardsRoutingTests:testReplicaOnOldestVersionIsPromotedDocRep/testReplicaOnOldestVersionIsPromotedSegRepnow assert oldest-versionpromotion for both replication types.
NodeVersionAllocationDeciderTests: addedtestAllocatesReplicaOnSameLuceneMinorDifferentOpenSearchPatch(2.19.4primary -> 2.19.0 target, both Lucene 9.12.x, expect YES),
testDoesNotAllocateReplicaOnOlderLuceneMinor(9.12 -> 9.11, expect NO),testDoesNotAllocateReplicaAcrossOlderLuceneMajor(Lucene 10 -> 9, expectNO). Existing
testMessagesassertions updated for the new wording.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.