This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/helix.git
The following commit(s) were added to refs/heads/master by this push:
new 08c6870bf Fix 2 reopened flaky CI tests (#3146, #3178)
08c6870bf is described below
commit 08c6870bf6d8adec1d2dff584ec52e50218ed4e7
Author: Junkai Xue <[email protected]>
AuthorDate: Wed Nov 4 14:06:50 2026 -0800
Fix 2 reopened flaky CI tests (#3146, #3178)
TestInstanceOperation.testNodeSwapBasic (#3146): after
completeSwapIfPossible
the test demanded an exact in-place copy of the pre-swap external view
(swap-in substituted for swap-out, everything else identical). CI showed
leaders and even membership moving on partitions unrelated to the pair once
the swap completed. Once the swap-in is ENABLE and the swap-out is UNKNOWN
the controller no longer pairs the two instances; the final assignment is an
ordinary rebalance over the new node set, so the mirror is only guaranteed
while the SWAP_IN is in flight (still asserted). Every post-completion check
of this kind (testNodeSwapBasic,
testSwapNotAffectedByDisableReenableOfSwapOut,
testSwapInCanBeSetAfterInstanceAdded, testSwapInDuringMaintenanceMode,
testDisabledPartitionsBeforeSwap, testDisabledPartitionsAfterSwap) now uses
validateReplacementAssignment, the check already adopted for the sibling
swap/evacuate tests in the previous round: swap-out holds nothing, every
partition fully replicated with exactly one top-state replica, swap-in hosts
load. testNodeSwapBasic re-enables the carried-over disabled partition
before
the final check, using the resource/partition overload (the deprecated 2-arg
overload it used before edits a legacy list field the controller does not
read, so that re-enable was a no-op). The two one-shot post-completion
validations now poll. waitForSwapProcessed now waits for the actual
post-completion signal (swap-out holds no replica, swap-in holds one)
instead
of insisting the swap-in land on exactly the swap-out's partitions, which
made
it spin for its full 30s on the same shift. Top-state lookups in
validateReplacementAssignment are cached since it runs inside 50ms polls.
TestConsecutiveZkSessionExpiry.testDistributedController (#3178): the double
session expiry left the cluster non-converged for the whole verifier window
(30s in Sept, 120s in Nov), so timeouts are not the issue. It did not
reproduce locally in 18 runs including a CPU-stressed loop, and the CI log
level is ERROR-only so nothing about the stuck state was captured. The test
now (1) waits for the expired manager to actually re-create its LIVEINSTANCE
under the new session instead of only for the ZK client's session id to
change, which lets the test start the convergence check from a known-good
point and separates "manager never re-joined" from "cluster never
converged", and (2) dumps leader, live instances, per-session current
states, pending messages and external views to stdout when either wait
fails, so the next CI failure is diagnosable. getSessionId() is guarded in
the poll and in the dump (it throws when the manager is disconnected) and
the dump tolerates znodes disappearing underneath it.
No production code changed.
---
.../manager/TestConsecutiveZkSessionExpiry.java | 166 +++++++++++++++++----
.../rebalancer/TestInstanceOperation.java | 138 ++++++++---------
2 files changed, 197 insertions(+), 107 deletions(-)
diff --git
a/helix-core/src/test/java/org/apache/helix/integration/manager/TestConsecutiveZkSessionExpiry.java
b/helix-core/src/test/java/org/apache/helix/integration/manager/TestConsecutiveZkSessionExpiry.java
index 0254073b7..57469a671 100644
---
a/helix-core/src/test/java/org/apache/helix/integration/manager/TestConsecutiveZkSessionExpiry.java
+++
b/helix-core/src/test/java/org/apache/helix/integration/manager/TestConsecutiveZkSessionExpiry.java
@@ -21,9 +21,11 @@ package org.apache.helix.integration.manager;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CountDownLatch;
import org.apache.helix.HelixDataAccessor;
+import org.apache.helix.HelixManager;
import org.apache.helix.PreConnectCallback;
import org.apache.helix.PropertyKey;
import org.apache.helix.TestHelper;
@@ -34,7 +36,10 @@ import org.apache.helix.manager.zk.CallbackHandler;
import org.apache.helix.manager.zk.ZKHelixDataAccessor;
import org.apache.helix.manager.zk.ZkBaseDataAccessor;
import org.apache.helix.mock.participant.MockMSModelFactory;
+import org.apache.helix.model.CurrentState;
+import org.apache.helix.model.ExternalView;
import org.apache.helix.model.LiveInstance;
+import org.apache.helix.model.Message;
import org.apache.helix.tools.ClusterStateVerifier;
import
org.apache.helix.tools.ClusterStateVerifier.BestPossAndExtViewZkVerifier;
import org.slf4j.Logger;
@@ -146,23 +151,12 @@ public class TestConsecutiveZkSessionExpiry extends
ZkUnitTestBase {
endCountdown.countDown();
- // Wait for the participant to fully reconnect with a new session before
verifying cluster
- // state. Recovering from consecutive session expiries (re-joining the
cluster and carrying
- // over the current states) can take a while on overloaded machines, and
the default 30s
- // verifier window was not enough.
- final MockParticipantManager expiredParticipant = participants[0];
- final String expiredSessionId = oldSessionId;
- Assert.assertTrue(TestHelper.verify(() -> {
- String currentSessionId = expiredParticipant.getSessionId();
- return expiredParticipant.isConnected() && currentSessionId != null
- && !currentSessionId.equals(expiredSessionId);
- }, TestHelper.WAIT_DURATION),
- "Participant should reconnect with a new session after consecutive
expiries");
-
- result =
- ClusterStateVerifier.verifyByPolling(new
ClusterStateVerifier.BestPossAndExtViewZkVerifier(
- ZK_ADDR, clusterName), 2 * TestHelper.WAIT_DURATION);
- Assert.assertTrue(result);
+ // Wait for the participant to fully re-join the cluster under its new
session before
+ // verifying cluster state. Recovering from consecutive session expiries
(re-joining the
+ // cluster and carrying over the current states) can take a while on
overloaded machines.
+ waitForRejoinWithNewSession(participants[0], clusterName, oldSessionId);
+
+ verifyClusterConverged(clusterName);
// clean up
controller.syncStop();
@@ -238,22 +232,12 @@ public class TestConsecutiveZkSessionExpiry extends
ZkUnitTestBase {
endCountdown.countDown();
- // Wait for the distributed controller to fully reconnect with a new
session before
- // verifying cluster state. Recovering from consecutive session expiries
can be slow
+ // Wait for the distributed controller to fully re-join the cluster under
its new session
+ // before verifying cluster state. Recovering from consecutive session
expiries can be slow
// on overloaded machines.
- final ClusterDistributedController expiredController =
distributedControllers[0];
- final String expiredSessionId = oldSessionId;
- Assert.assertTrue(TestHelper.verify(() -> {
- String currentSessionId = expiredController.getSessionId();
- return expiredController.isConnected() && currentSessionId != null
- && !currentSessionId.equals(expiredSessionId);
- }, TestHelper.WAIT_DURATION),
- "Distributed controller should reconnect with a new session after
consecutive expiries");
-
- result =
- ClusterStateVerifier.verifyByPolling(new
ClusterStateVerifier.BestPossAndExtViewZkVerifier(
- ZK_ADDR, clusterName), 2 * TestHelper.WAIT_DURATION);
- Assert.assertTrue(result);
+ waitForRejoinWithNewSession(distributedControllers[0], clusterName,
oldSessionId);
+
+ verifyClusterConverged(clusterName);
// verify leader changes to localhost_12919
HelixDataAccessor accessor =
@@ -301,4 +285,122 @@ public class TestConsecutiveZkSessionExpiry extends
ZkUnitTestBase {
System.out.println("END " + clusterName + " at " + new
Date(System.currentTimeMillis()));
}
+ /**
+ * Waits until the manager has re-joined the cluster under a session other
than the expired one:
+ * the ZK client is connected with a new session id AND the LIVEINSTANCE
znode has been
+ * re-created by that session. The second condition is what matters for the
cluster state
+ * verification that follows: it means HelixManager#handleNewSession() for
the surviving session
+ * got past the live instance creation (the point where the handler for the
intermediate,
+ * expired-while-being-handled session fails by design in this test). Only
checking the client's
+ * session id would let the test proceed while the manager is still
(re)initializing.
+ */
+ private void waitForRejoinWithNewSession(final HelixManager manager, String
clusterName,
+ final String expiredSessionId) throws Exception {
+ final HelixDataAccessor accessor = accessor(clusterName);
+ final PropertyKey liveInstanceKey =
+ accessor.keyBuilder().liveInstance(manager.getInstanceName());
+ boolean rejoined = TestHelper.verify(() -> {
+ // getSessionId() throws when the manager is (or becomes) disconnected;
treat that as
+ // "not re-joined yet" and keep polling instead of aborting the wait.
+ String currentSessionId = currentSessionIdOrNull(manager);
+ if (currentSessionId == null ||
currentSessionId.equals(expiredSessionId)) {
+ return false;
+ }
+ LiveInstance liveInstance = accessor.getProperty(liveInstanceKey);
+ return liveInstance != null &&
currentSessionId.equals(liveInstance.getEphemeralOwner());
+ }, TestHelper.WAIT_DURATION);
+ if (!rejoined) {
+ dumpClusterState(clusterName, "instance " + manager.getInstanceName()
+ + " did not re-join with a new session (expired session: " +
expiredSessionId
+ + ", current session: " + currentSessionIdOrNull(manager) + ",
connected: "
+ + manager.isConnected() + ")");
+ }
+ Assert.assertTrue(rejoined, manager.getInstanceName()
+ + " should re-create its live instance with a new session after
consecutive expiries");
+ }
+
+ /**
+ * Verifies that the external views converge to the best possible state. On
failure, dumps the
+ * relevant cluster state to stdout (the CI log level is ERROR only, so test
logging is not
+ * available there) before failing, so that a non-converging cluster can be
diagnosed from the
+ * CI output instead of just "expected:<true> but was:<false>".
+ */
+ private void verifyClusterConverged(String clusterName) {
+ boolean result =
+ ClusterStateVerifier.verifyByPolling(new
ClusterStateVerifier.BestPossAndExtViewZkVerifier(
+ ZK_ADDR, clusterName), 2 * TestHelper.WAIT_DURATION);
+ if (!result) {
+ dumpClusterState(clusterName, "cluster did not converge after
consecutive session expiries");
+ }
+ Assert.assertTrue(result,
+ "Cluster should converge to the best possible state after consecutive
session expiries");
+ }
+
+ private HelixDataAccessor accessor(String clusterName) {
+ return new ZKHelixDataAccessor(clusterName, new
ZkBaseDataAccessor<ZNRecord>(_gZkClient));
+ }
+
+ private static String currentSessionIdOrNull(HelixManager manager) {
+ try {
+ return manager.isConnected() ? manager.getSessionId() : null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Best effort: every read below tolerates znodes disappearing underneath it
(which is exactly
+ * what happens around a session expiry) so that one failed section does not
hide the others.
+ */
+ private void dumpClusterState(String clusterName, String reason) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("==== ").append(clusterName).append(":
").append(reason).append(" ====\n");
+ try {
+ HelixDataAccessor accessor = accessor(clusterName);
+ PropertyKey.Builder keyBuilder = accessor.keyBuilder();
+
+ LiveInstance leader =
accessor.getProperty(keyBuilder.controllerLeader());
+ sb.append("LEADER: ").append(leader == null ? "none"
+ : leader.getInstanceName() + " (session " +
leader.getEphemeralOwner() + ")")
+ .append('\n');
+
+ Map<String, LiveInstance> liveInstances =
+ accessor.getChildValuesMap(keyBuilder.liveInstances(), false);
+ for (Map.Entry<String, LiveInstance> entry : liveInstances.entrySet()) {
+ sb.append("LIVEINSTANCE ").append(entry.getKey()).append(": session ")
+ .append(entry.getValue().getEphemeralOwner()).append('\n');
+ }
+
+ for (String instance : accessor.getChildNames(keyBuilder.instances())) {
+ for (String session :
accessor.getChildNames(keyBuilder.sessions(instance))) {
+ List<CurrentState> currentStates =
+ accessor.getChildValues(keyBuilder.currentStates(instance,
session), false);
+ for (CurrentState currentState : currentStates) {
+ sb.append("CURRENTSTATE
").append(instance).append('/').append(session).append('/')
+ .append(currentState.getResourceName()).append(": ")
+ .append(currentState.getPartitionStateMap()).append('\n');
+ }
+ }
+ List<Message> messages =
accessor.getChildValues(keyBuilder.messages(instance), false);
+ for (Message message : messages) {
+ sb.append("MESSAGE
").append(instance).append('/').append(message.getMsgId())
+ .append(": ").append(message.getMsgType()).append(' ')
+
.append(message.getResourceName()).append('/').append(message.getPartitionName())
+ .append('
').append(message.getFromState()).append("->").append(message.getToState())
+ .append("
tgtSession=").append(message.getTgtSessionId()).append(" state=")
+ .append(message.getMsgState()).append('\n');
+ }
+ }
+
+ List<ExternalView> externalViews =
+ accessor.getChildValues(keyBuilder.externalViews(), false);
+ for (ExternalView externalView : externalViews) {
+ sb.append("EXTERNALVIEW
").append(externalView.getResourceName()).append(": ")
+ .append(externalView.getRecord().getMapFields()).append('\n');
+ }
+ } catch (Exception e) {
+ sb.append("failed to dump cluster state: ").append(e).append('\n');
+ }
+ System.out.println(sb);
+ }
}
diff --git
a/helix-core/src/test/java/org/apache/helix/integration/rebalancer/TestInstanceOperation.java
b/helix-core/src/test/java/org/apache/helix/integration/rebalancer/TestInstanceOperation.java
index 35f874958..84cc3117d 100644
---
a/helix-core/src/test/java/org/apache/helix/integration/rebalancer/TestInstanceOperation.java
+++
b/helix-core/src/test/java/org/apache/helix/integration/rebalancer/TestInstanceOperation.java
@@ -123,6 +123,8 @@ public class TestInstanceOperation extends ZkTestBase {
// Test state
protected ClusterControllerManager _controller;
+ // Top state per resource, looked up once; validateReplacementAssignment
runs inside 50ms polls.
+ private final Map<String, String> _topStateByResource = new HashMap<>();
private HelixManager _spectator;
private RoutingTableProvider _routingTableProviderDefault;
private RoutingTableProvider _routingTableProviderEV;
@@ -556,12 +558,12 @@ public class TestInstanceOperation extends ZkTestBase {
// Disable one partition on swap-out for testing
String resourceToDisable = _allDBs.iterator().next();
- getPartitionsAndStatesOnInstance(getEVs(), swapOutName).entrySet().stream()
- .filter(entry -> entry.getKey().startsWith(resourceToDisable))
- .findFirst()
- .ifPresent(entry -> {
- swapOutConfig.setInstanceEnabledForPartition(resourceToDisable,
entry.getKey(), false);
- });
+ String partitionToDisable = getPartitionsAndStatesOnInstance(getEVs(),
swapOutName).keySet()
+ .stream().filter(partition ->
partition.startsWith(resourceToDisable)).findFirst()
+ .orElse(null);
+ Assert.assertNotNull(partitionToDisable,
+ "Swap-out " + swapOutName + " should host a partition of " +
resourceToDisable);
+ swapOutConfig.setInstanceEnabledForPartition(resourceToDisable,
partitionToDisable, false);
_gSetupTool.getClusterManagementTool()
.setInstanceConfig(CLUSTER_NAME, swapOutName, swapOutConfig);
@@ -606,7 +608,7 @@ public class TestInstanceOperation extends ZkTestBase {
// Wait for swap to be fully processed and EV to stabilize
// After completeSwapIfPossible, the controller needs time to update the EV
- waitForSwapProcessed(originalEVs, swapOutName, swapInName);
+ waitForSwapProcessed(swapOutName, swapInName);
// Verify swap-in is now in routing tables
validateRoutingTablesInstance(swapInName, true);
@@ -624,17 +626,24 @@ public class TestInstanceOperation extends ZkTestBase {
Assert.assertTrue(configListener.isThrottlesEnabled(),
"Throttles should be re-enabled after swap");
- // Verify final EV is correct
- verifier(() -> validateEVsCorrect(getEVs(), originalEVs, swapOutToSwapIn,
- Collections.emptySet(), ImmutableSet.of(swapInName)),
VERIFICATION_TIMEOUT);
-
- // Re-enable the disabled partition on swap-in
+ // The disabled partition was carried over from the swap-out's config.
Re-enable it on the
+ // swap-in (same resource/partition overload as the disable above; the
deprecated 2-arg
+ // overload edits a legacy list field the controller does not read) so the
final assignment
+ // can be validated as fully healthy.
InstanceConfig swapInConfig = _gSetupTool.getClusterManagementTool()
.getInstanceConfig(CLUSTER_NAME, swapInName);
- swapInConfig.setInstanceEnabledForPartition(resourceToDisable, true);
+ swapInConfig.setInstanceEnabledForPartition(resourceToDisable,
partitionToDisable, true);
_gSetupTool.getClusterManagementTool()
.setInstanceConfig(CLUSTER_NAME, swapInName, swapInConfig);
+ // Verify final EV. Once the swap is completed the swap-in is a regular
ENABLE instance and
+ // the swap-out is UNKNOWN, so the controller no longer pairs them and the
final assignment is
+ // an ordinary rebalance over the new node set, not an in-place copy of
the pre-swap EV (see
+ // validateReplacementAssignment). The exact mirror is asserted above
while the swap is in
+ // flight, which is the only phase where the product guarantees it.
+ verifier(() -> validateReplacementAssignment(getEVs(), originalEVs,
swapOutName, swapInName),
+ VERIFICATION_TIMEOUT * 2);
+
LOG.info("END testNodeSwapBasic");
}
@@ -701,16 +710,18 @@ public class TestInstanceOperation extends ZkTestBase {
"Swap should complete");
// Wait for swap to be fully processed and EV to stabilize
- waitForSwapProcessed(originalEVs, swapOutName, swapInName);
+ waitForSwapProcessed(swapOutName, swapInName);
// Verify swap-out is disabled
Assert.assertFalse(_gSetupTool.getClusterManagementTool()
.getInstanceConfig(CLUSTER_NAME, swapOutName).getInstanceEnabled(),
"Swap-out should be disabled");
- // Verify final EV
- verifier(() -> validateEVsCorrect(getEVs(), originalEVs, swapOutToSwapIn,
- Collections.emptySet(), ImmutableSet.of(swapInName)),
VERIFICATION_TIMEOUT);
+ // Verify final EV. The exact pre-swap mirror is only guaranteed while the
swap is in flight
+ // (asserted above); after completion the pair is no longer tracked and
the assignment is
+ // recomputed over the new node set, so check the product's guarantees
instead.
+ verifier(() -> validateReplacementAssignment(getEVs(), originalEVs,
swapOutName, swapInName),
+ VERIFICATION_TIMEOUT * 2);
LOG.info("END testSwapNotAffectedByDisableReenableOfSwapOut");
}
@@ -756,9 +767,10 @@ public class TestInstanceOperation extends ZkTestBase {
Assert.assertTrue(_clusterVerifier.verifyByPolling());
- // Verify final state
- verifier(() -> validateEVsCorrect(getEVs(), originalEVs, swapOutToSwapIn,
- Collections.emptySet(), ImmutableSet.of(swapInName)),
VERIFICATION_TIMEOUT);
+ // Verify final state. See testNodeSwapBasic: after completion the exact
pre-swap mirror is
+ // not guaranteed, only a healthy replacement assignment is.
+ verifier(() -> validateReplacementAssignment(getEVs(), originalEVs,
swapOutName, swapInName),
+ VERIFICATION_TIMEOUT * 2);
LOG.info("END testSwapInCanBeSetAfterInstanceAdded");
}
@@ -878,9 +890,10 @@ public class TestInstanceOperation extends ZkTestBase {
// Verify swap-in in routing tables
validateRoutingTablesInstance(swapInName, true);
- // Verify final EV
- verifier(() -> validateEVsCorrect(getEVs(), originalEVs, swapOutToSwapIn,
- Collections.emptySet(), ImmutableSet.of(swapInName)),
VERIFICATION_TIMEOUT);
+ // Verify final EV. See testNodeSwapBasic: after completion the exact
pre-swap mirror is
+ // not guaranteed, only a healthy replacement assignment is.
+ verifier(() -> validateReplacementAssignment(getEVs(), originalEVs,
swapOutName, swapInName),
+ VERIFICATION_TIMEOUT * 2);
LOG.info("END testSwapInDuringMaintenanceMode");
}
@@ -1289,8 +1302,11 @@ public class TestInstanceOperation extends ZkTestBase {
"Swap should complete");
Assert.assertTrue(_clusterVerifier.verifyByPolling());
- validateEVsCorrect(getEVs(), beforeEVs, swapOutToSwapIn,
- Collections.emptySet(), ImmutableSet.of(swapInName));
+ // See testNodeSwapBasic: after completion the exact pre-swap mirror is
not guaranteed, only a
+ // healthy replacement assignment is. The one-shot read is also replaced
by polling because
+ // the EV is rewritten asynchronously after completeSwapIfPossible.
+ verifier(() -> validateReplacementAssignment(getEVs(), beforeEVs,
swapOutName, swapInName),
+ VERIFICATION_TIMEOUT * 2);
// Cleanup
_participants.get(_participants.size() - 1).syncStop();
@@ -1375,8 +1391,10 @@ public class TestInstanceOperation extends ZkTestBase {
"Swap should complete");
Assert.assertTrue(_clusterVerifier.verifyByPolling());
- validateEVsCorrect(getEVs(), beforeSwapDisableEVs, swapOutToSwapIn,
- Collections.emptySet(), ImmutableSet.of(swapInName));
+ // See testNodeSwapBasic: after completion the exact pre-swap mirror is
not guaranteed, only a
+ // healthy replacement assignment is. Polled because the EV is rewritten
asynchronously.
+ verifier(() -> validateReplacementAssignment(getEVs(),
beforeSwapDisableEVs, swapOutName,
+ swapInName), VERIFICATION_TIMEOUT * 2);
// Cleanup
_participants.get(_participants.size() - 1).syncStop();
@@ -2059,56 +2077,25 @@ public class TestInstanceOperation extends ZkTestBase {
/**
* Waits (up to 30s) for a completed swap to be reflected in the external
views: the swap-out
- * instance no longer holds LEADER where it originally did, and the swap-in
instance has taken
- * over every partition the swap-out instance originally hosted. Only logs a
warning on timeout;
- * callers are expected to run their own definitive validations afterwards.
+ * instance holds no replica anymore and the swap-in instance hosts at least
one. Only logs a
+ * warning on timeout; callers are expected to run their own definitive
validations afterwards.
*/
- private void waitForSwapProcessed(Map<String, ExternalView> originalEVs,
String swapOutName,
- String swapInName) throws InterruptedException {
+ private void waitForSwapProcessed(String swapOutName, String swapInName)
+ throws InterruptedException {
long swapWaitStartTime = System.currentTimeMillis();
long maxSwapWaitTime = 30000;
boolean swapProcessed = false;
while (System.currentTimeMillis() - swapWaitStartTime < maxSwapWaitTime) {
if (_clusterVerifier.verifyByPolling()) {
Map<String, ExternalView> currentEVs = getEVs();
- boolean allCorrect = true;
- for (String resource : originalEVs.keySet()) {
- ExternalView current = currentEVs.get(resource);
- ExternalView original = originalEVs.get(resource);
- if (current == null || original == null) {
- allCorrect = false;
- break;
- }
- for (String partition : original.getPartitionSet()) {
- Map<String, String> currentStateMap =
current.getStateMap(partition);
- Map<String, String> originalStateMap =
original.getStateMap(partition);
- // The partition may be transiently missing from an EV while the
controller
- // rewrites it; keep polling instead of throwing NPE out of the
wait loop.
- if (currentStateMap == null || originalStateMap == null) {
- allCorrect = false;
- break;
- }
- // Check if swapOut is still in EV with LEADER state (should be
replaced)
- if (currentStateMap.containsKey(swapOutName)) {
- String currentState = currentStateMap.get(swapOutName);
- String originalState = originalStateMap.get(swapOutName);
- if (currentState.equals(originalState) &&
"LEADER".equals(currentState)) {
- allCorrect = false;
- break;
- }
- }
- // Check if swapIn has taken over the replicas swapOut originally
had.
- // Only partitions that were on swapOut are expected on swapIn;
requiring
- // swapIn in every partition made this wait condition
unsatisfiable.
- if (originalStateMap.containsKey(swapOutName)
- && !currentStateMap.containsKey(swapInName)) {
- allCorrect = false;
- break;
- }
- }
- if (!allCorrect) break;
- }
- if (allCorrect) {
+ // A partition may be transiently missing from an EV while the
controller rewrites it;
+ // treat a null state map as "not hosted" instead of throwing out of
the wait loop.
+ boolean swapOutGone = currentEVs.values().stream().allMatch(ev -> ev
!= null
+ && ev.getPartitionSet().stream().allMatch(partition ->
ev.getStateMap(partition) == null
+ || !ev.getStateMap(partition).containsKey(swapOutName)));
+ boolean swapInPresent = currentEVs.values().stream()
+ .anyMatch(ev -> ev != null &&
getParticipantsInEv(ev).contains(swapInName));
+ if (swapOutGone && swapInPresent) {
swapProcessed = true;
LOG.info("Swap processed and EV stabilized after {} ms",
System.currentTimeMillis() - swapWaitStartTime);
@@ -2147,11 +2134,12 @@ public class TestInstanceOperation extends ZkTestBase {
Assert.assertEquals(actual.getPartitionSet(), original.getPartitionSet(),
"Partition set should match for resource " + resource);
- IdealState is = _gSetupTool.getClusterManagementTool()
- .getResourceIdealState(CLUSTER_NAME, resource);
- StateModelDefinition stateModelDef =
_gSetupTool.getClusterManagementTool()
- .getStateModelDef(CLUSTER_NAME, is.getStateModelDefRef());
- String topState = stateModelDef.getTopState();
+ String topState = _topStateByResource.computeIfAbsent(resource, r -> {
+ IdealState is = _gSetupTool.getClusterManagementTool()
+ .getResourceIdealState(CLUSTER_NAME, r);
+ return _gSetupTool.getClusterManagementTool()
+ .getStateModelDef(CLUSTER_NAME,
is.getStateModelDefRef()).getTopState();
+ });
for (String partition : original.getPartitionSet()) {
Map<String, String> stateMap = actual.getStateMap(partition);