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 1bc679e2e Another round of flaky test fixes
1bc679e2e is described below

commit 1bc679e2e00661199b1ed73b6ed04a7cf3c3d06f
Author: Junkai Xue <[email protected]>
AuthorDate: Mon Oct 19 13:41:53 2026 -0700

    Another round of flaky test fixes
---
 .../rebalancer/TestInstanceOperation.java          | 125 ++++++++++++---------
 .../apache/helix/manager/zk/TestHandleSession.java | 106 ++++++++++-------
 .../messaging/handling/TestHelixTaskExecutor.java  |  10 +-
 3 files changed, 144 insertions(+), 97 deletions(-)

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 e0c14cae5..e06c3a947 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
@@ -1030,7 +1030,6 @@ public class TestInstanceOperation extends ZkTestBase {
     removeOfflineOrInactiveInstances();
     Assert.assertTrue(_clusterVerifier.verifyByPolling());
 
-    Map<String, String> swapOutToSwapIn = new HashMap<>();
     Map<String, ExternalView> originalEVs = getEVs();
 
     // Enter maintenance mode
@@ -1058,7 +1057,6 @@ public class TestInstanceOperation extends ZkTestBase {
     // Add new instance with ENABLE
     String swapInName = PARTICIPANT_PREFIX + "_" + _nextStartPort;
     LOG.info("Adding new instance: {}", swapInName);
-    swapOutToSwapIn.put(swapOutName, swapInName);
     addParticipant(swapInName, swapOutConfig.getLogicalId(LOGICAL_ID),
         swapOutConfig.getDomainAsMap().get(ZONE),
         InstanceConstants.InstanceOperation.ENABLE, -1);
@@ -1070,56 +1068,17 @@ public class TestInstanceOperation extends ZkTestBase {
 
     Assert.assertTrue(_clusterVerifier.verifyByPolling());
 
-    // Ensure swap is fully processed after maintenance mode ends
-    // During maintenance mode, EV updates are deferred. After exiting, we 
need to wait
-    // for the controller to compute and propagate the new ideal state 
assignment.
-    long swapWaitStartTime = System.currentTimeMillis();
-    long maxSwapWaitTime = 30000;
-    boolean swapProcessed = false;
-    while (System.currentTimeMillis() - swapWaitStartTime < maxSwapWaitTime) {
-      Map<String, ExternalView> currentEVs = getEVs();
-      boolean allReplaced = true;
-      for (String resource : originalEVs.keySet()) {
-        ExternalView current = currentEVs.get(resource);
-        ExternalView original = originalEVs.get(resource);
-        if (current == null || original == null) {
-          allReplaced = false;
-          break;
-        }
-        for (String partition : original.getPartitionSet()) {
-          Map<String, String> currentStateMap = current.getStateMap(partition);
-          Map<String, String> originalStateMap = 
original.getStateMap(partition);
-          // Check if swapOut is still in EV with higher state than expected
-          if (currentStateMap.containsKey(swapOutName)) {
-            String currentState = currentStateMap.get(swapOutName);
-            String originalState = originalStateMap.get(swapOutName);
-            if (currentState.equals(originalState) && 
"LEADER".equals(currentState)) {
-              // swapOut still has its original (higher) state - not yet 
replaced
-              allReplaced = false;
-              break;
-            }
-          }
-        }
-        if (!allReplaced) break;
-      }
-      if (allReplaced) {
-        swapProcessed = true;
-        LOG.info("Swap processed after {} ms", System.currentTimeMillis() - 
swapWaitStartTime);
-        break;
-      }
-      Thread.sleep(500);
-    }
-    if (!swapProcessed) {
-      LOG.warn("Swap may not have been fully processed within {} ms", 
maxSwapWaitTime);
-    }
-
-    // Verify swap-in has the same partitions swap-out had.
-    // Use verifier() instead of TestHelper.verify directly: 
validateEVsCorrect asserts
-    // internally, and an AssertionError thrown from the lambda escapes 
TestHelper.verify's
-    // retry loop, failing the test on the first transient mismatch instead of 
polling
-    // until the cluster converges.
-    verifier(() -> validateEVsCorrect(getEVs(), originalEVs, swapOutToSwapIn,
-        Collections.emptySet(), ImmutableSet.of(swapInName)), 
VERIFICATION_TIMEOUT * 2);
+    // The new instance is added with ENABLE, not SWAP_IN, so the controller 
does not pair it with
+    // the evacuated instance: it is just another assignable node that happens 
to share the
+    // evacuated node's logicalId (and therefore its position in the CRUSH 
topology). The product
+    // only guarantees that the evacuated instance ends up with no replicas 
while every partition
+    // keeps a full, healthy replica set, and that the new instance picks up 
load. It does not
+    // guarantee an exact in-place copy of the evacuated instance's 
assignment, since the even
+    // distribution rounds and preference-list order can legitimately differ 
once the node set
+    // changes (observed on CI as leaders moving on unrelated partitions). 
Only the SWAP_IN flow
+    // (see the testSwap* tests) mirrors the swap-out assignment onto the 
swap-in instance.
+    verifier(() -> validateEvacuateReplacement(getEVs(), originalEVs, 
swapOutName, swapInName),
+        VERIFICATION_TIMEOUT * 2);
 
     // Verify evacuation finished - use verifier to handle timing variations
     boolean evacFinished = TestHelper.verify(() ->
@@ -1173,10 +1132,10 @@ public class TestInstanceOperation extends ZkTestBase {
 
     Assert.assertTrue(_clusterVerifier.verifyByPolling());
 
-    // Final validation. verifier() tolerates AssertionError from 
validateEVsCorrect and
-    // keeps polling until the cluster converges.
-    verifier(() -> validateEVsCorrect(getEVs(), originalEVs, swapOutToSwapIn,
-        Collections.emptySet(), ImmutableSet.of(swapInName)), 
VERIFICATION_TIMEOUT * 2);
+    // Final validation: the evacuated instance (now UNKNOWN) still holds 
nothing, every partition
+    // is fully replicated and the new instance keeps its load.
+    verifier(() -> validateEvacuateReplacement(getEVs(), originalEVs, 
swapOutName, swapInName),
+        VERIFICATION_TIMEOUT * 2);
 
     LOG.info("END testEvacuateWithAddDuringMaintenanceMode");
   }
@@ -2157,6 +2116,60 @@ public class TestInstanceOperation extends ZkTestBase {
     }
   }
 
+  /**
+   * Validates the external views after an instance was evacuated and a 
replacement instance was
+   * added with ENABLE (not SWAP_IN). Unlike {@link #validateEVsCorrect}, this 
does not expect an
+   * exact in-place copy of the evacuated instance's assignment, because an 
ENABLE instance is not
+   * paired with the evacuated one by the controller and the rebalancer may 
legitimately pick a
+   * different (still valid) assignment and preference-list order. It checks 
what EVACUATE does
+   * guarantee: the evacuated instance holds no replicas, every original 
partition still has a
+   * full set of healthy replicas with exactly one top-state replica, and the 
replacement instance
+   * has taken on load.
+   */
+  private boolean validateEvacuateReplacement(Map<String, ExternalView> 
actuals,
+      Map<String, ExternalView> originals, String evacuatedInstance, String 
replacementInstance) {
+    Assert.assertEquals(actuals.keySet(), originals.keySet(), "Resource key 
sets should match");
+
+    boolean replacementHasReplicas = false;
+    for (String resource : originals.keySet()) {
+      ExternalView actual = actuals.get(resource);
+      ExternalView original = originals.get(resource);
+      Assert.assertNotNull(actual, "Missing external view for resource " + 
resource);
+      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();
+
+      for (String partition : original.getPartitionSet()) {
+        Map<String, String> stateMap = actual.getStateMap(partition);
+        Assert.assertNotNull(stateMap,
+            String.format("Partition %s in resource %s has no state map", 
partition, resource));
+        Assert.assertFalse(stateMap.containsKey(evacuatedInstance),
+            String.format("Evacuated instance %s still hosts partition %s in 
resource %s: %s",
+                evacuatedInstance, partition, resource, stateMap));
+        Assert.assertEquals(stateMap.size(), REPLICA,
+            String.format("Partition %s in resource %s should have %d 
replicas: %s", partition,
+                resource, REPLICA, stateMap));
+        
Assert.assertTrue(stateMap.values().stream().allMatch(ACCEPTABLE_STATE_SET::contains),
+            String.format("Partition %s in resource %s has unhealthy replicas: 
%s", partition,
+                resource, stateMap));
+        
Assert.assertEquals(stateMap.values().stream().filter(topState::equals).count(),
 1L,
+            String.format("Partition %s in resource %s should have exactly one 
%s replica: %s",
+                partition, resource, topState, stateMap));
+        if (stateMap.containsKey(replacementInstance)) {
+          replacementHasReplicas = true;
+        }
+      }
+    }
+    Assert.assertTrue(replacementHasReplicas,
+        "Replacement instance " + replacementInstance + " should host some 
replicas");
+    return true;
+  }
+
   private boolean validateEVsCorrect(Map<String, ExternalView> actuals,
       Map<String, ExternalView> originals, Map<String, String> swapOutToSwapIn,
       Set<String> inFlightSwapIn, Set<String> completedSwapIn) {
diff --git 
a/helix-core/src/test/java/org/apache/helix/manager/zk/TestHandleSession.java 
b/helix-core/src/test/java/org/apache/helix/manager/zk/TestHandleSession.java
index 2c0838a66..8e15bfa17 100644
--- 
a/helix-core/src/test/java/org/apache/helix/manager/zk/TestHandleSession.java
+++ 
b/helix-core/src/test/java/org/apache/helix/manager/zk/TestHandleSession.java
@@ -26,6 +26,7 @@ import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.helix.AccessOption;
 import org.apache.helix.HelixException;
@@ -50,6 +51,10 @@ import org.testng.annotations.Test;
 
 
 public class TestHandleSession extends ZkTestBase {
+  // Generous polling window for session re-establishment, handler reset and 
live instance
+  // creation. These are fast normally but take seconds on slow CI machines.
+  private static final long SESSION_WAIT_DURATION = 30 * 1000L;
+
   private static final String _className = TestHelper.getTestClassName();
 
   @Test
@@ -397,16 +402,27 @@ public class TestHandleSession extends ZkTestBase {
 
     final int handlerCount = manager.getHandlers().size();
     final long originalCreationTime = liveInstance.getStat().getCreationTime();
-    final CountDownLatch mainThreadBlocker = new CountDownLatch(1);
+    // Released by the helper thread once expired S1 has completed resetting 
handlers and S2 has
+    // started handling its new session, so the main thread can verify S1 did 
not create the live
+    // instance.
+    final CountDownLatch s1ResetDoneBlocker = new CountDownLatch(1);
+    // Released by the main thread to let the helper thread proceed S2.
     final CountDownLatch helperThreadBlocker = new CountDownLatch(1);
+    // Released by the helper thread once it has let S2 proceed.
+    final CountDownLatch s2ProceededBlocker = new CountDownLatch(1);
+    // Failures in the helper thread are re-thrown on the main thread. They 
used to be swallowed,
+    // so a slow session establishment silently turned into a confusing 
failure of the final
+    // assertions.
+    final AtomicReference<Throwable> helperFailure = new AtomicReference<>();
 
     // Helper thread to help verify zk session states, async expire S1, 
proceed S1 to reset
     // handlers and release main thread to verify results.
     new Thread(() -> {
       try {
         // Wait for new session S1 is established and starting to reset 
handlers.
-        TestHelper.verify(() -> 
!(manager.getSessionId().equals(originalSessionId))
-            && manager.getResetHandlersStartTime() > 
initResetHandlersStartTime, 5000L);
+        Assert.assertTrue(TestHelper.verify(() -> 
!(manager.getSessionId().equals(originalSessionId))
+            && manager.getResetHandlersStartTime() > 
initResetHandlersStartTime,
+            SESSION_WAIT_DURATION), "S1 did not start resetting handlers in 
time");
 
         // S1's info.
         final String lastSessionId = manager.getSessionId();
@@ -417,50 +433,53 @@ public class TestHandleSession extends ZkTestBase {
           // 4. S1 expired, new session S2 created
           ZkTestHelper.asyncExpireSession(manager.getZkClient());
 
-          // Wait and verify the new session S2 is established.
-          TestHelper.verify(() -> 
!((ZKUtil.toHexSessionId(manager.getZkClient().getSessionId()))
-              .equals(lastSessionId)), 5000L);
-        } catch (Exception ignored) {
-          // Ignored.
+          // Wait and verify the zookeeper is closed. The zk client cannot 
reconnect while the
+          // event lock is held, so S2 only gets established after the unlock 
below.
+          Assert.assertTrue(TestHelper.verify(
+              () -> !((ZkClient) 
manager.getZkClient()).getConnection().getZookeeperState()
+                  .isAlive(), SESSION_WAIT_DURATION), "S1 was not expired in 
time");
         } finally {
           // Unlock to start processing event again.
           ((ZkClient) manager.getZkClient()).getEventLock().unlock();
         }
 
+        // Wait and verify the new session S2 is established.
+        Assert.assertTrue(TestHelper.verify(() -> {
+          try {
+            final String sessionId = 
ZKUtil.toHexSessionId(manager.getZkClient().getSessionId());
+            return !"0".equals(sessionId) && !sessionId.equals(lastSessionId);
+          } catch (HelixException ex) {
+            // The zk client may still be re-connecting.
+            return false;
+          }
+        }, SESSION_WAIT_DURATION), "S2 was not established in time");
+
         // Proceed S1 to complete reset handlers and try to create live 
instance.
         manager.proceedResetHandlers();
 
         // Wait for S2 to handle new session.
-        TestHelper.verify(() -> !(manager.getSessionId().equals(lastSessionId))
-            && manager.getResetHandlersStartTime() > 
lastResetHandlersStartTime, 5000L);
-
+        Assert.assertTrue(TestHelper.verify(() -> 
!(manager.getSessionId().equals(lastSessionId))
+            && manager.getResetHandlersStartTime() > 
lastResetHandlersStartTime,
+            SESSION_WAIT_DURATION), "S2 did not start resetting handlers in 
time");
+      } catch (Throwable t) {
+        helperFailure.compareAndSet(null, t);
+      } finally {
         // Notify main thread to verify result: expired S1 should not create 
live instance.
-        mainThreadBlocker.countDown();
+        s1ResetDoneBlocker.countDown();
+      }
 
+      try {
         // Wait for notification from main thread to proceed S2.
         helperThreadBlocker.await();
 
         // Proceed S2.
         // 6. S2 is valid and creates live instance.
         manager.proceedResetHandlers();
-
-        final String latestSessionId = 
ZKUtil.toHexSessionId(manager.getZkClient().getSessionId());
-
-        TestHelper.verify(() -> {
-          // Newly created live instance should be created by the latest 
session
-          // and have a new creation time.
-          LiveInstance newLiveInstance =
-              accessor.getProperty(keyBuilder.liveInstance(instanceName));
-          return newLiveInstance != null
-              && newLiveInstance.getStat().getCreationTime() != 
originalCreationTime
-              && newLiveInstance.getEphemeralOwner().equals(latestSessionId);
-        }, 2000L);
-      } catch (Exception ignored) {
-        // Ignored.
+      } catch (Throwable t) {
+        helperFailure.compareAndSet(null, t);
+      } finally {
+        s2ProceededBlocker.countDown();
       }
-
-      // Notify the main thread that live instance is already created by 
session S2.
-      mainThreadBlocker.countDown();
     }).start();
 
     // Lock zk event processing to simulate a long backlog queue.
@@ -473,14 +492,15 @@ public class TestHandleSession extends ZkTestBase {
       // Wait and verify the zookeeper is closed.
       Assert.assertTrue(TestHelper.verify(
           () -> !((ZkClient) 
manager.getZkClient()).getConnection().getZookeeperState().isAlive(),
-          3000L));
+          SESSION_WAIT_DURATION));
     } finally {
       // Unlock to start processing event again.
       ((ZkClient) manager.getZkClient()).getEventLock().unlock();
     }
 
     // Wait for S1 completing resetting handlers.
-    mainThreadBlocker.await();
+    s1ResetDoneBlocker.await();
+    Assert.assertNull(helperFailure.get(), "Helper thread failed: " + 
helperFailure.get());
 
     // 5. S1 completes resetting handlers, live instance should not be created 
by the expired S1
     
Assert.assertNull(accessor.getProperty(keyBuilder.liveInstance(instanceName)));
@@ -488,13 +508,17 @@ public class TestHandleSession extends ZkTestBase {
     // Notify helper thread to proceed S2.
     helperThreadBlocker.countDown();
 
-    // Wait for live instance being created by the new session S2.
-    mainThreadBlocker.await();
+    // Wait for the helper thread to let S2 proceed. (The previous version 
re-used an already
+    // released latch here, so it did not wait at all and then only gave S2 a 
2s window.)
+    s2ProceededBlocker.await();
+    Assert.assertNull(helperFailure.get(), "Helper thread failed: " + 
helperFailure.get());
 
-    // From now on, the live instance is already created by S2.
+    // From now on, the live instance is going to be created by S2.
     // The latest(the final new one S2) session id that is valid.
     final String latestSessionId = 
ZKUtil.toHexSessionId(manager.getZkClient().getSessionId());
 
+    // Finishing the handler reset and writing the live instance to ZK can 
take seconds on a slow
+    // machine, so poll generously.
     Assert.assertTrue(TestHelper.verify(() -> {
       // Newly created live instance should be created by the latest session
       // and have a new creation time.
@@ -502,12 +526,14 @@ public class TestHandleSession extends ZkTestBase {
       return newLiveInstance != null
           && newLiveInstance.getStat().getCreationTime() != 
originalCreationTime && newLiveInstance
           .getEphemeralOwner().equals(latestSessionId);
-    }, 2000L));
+    }, SESSION_WAIT_DURATION));
 
     // All the callback handlers shall be recovered.
-    Assert.assertTrue(TestHelper.verify(() -> manager.getHandlers().size() == 
handlerCount, 1000L));
-    Assert.assertTrue(TestHelper
-        .verify(() -> 
manager.getHandlers().stream().allMatch(CallbackHandler::isReady), 3000L));
+    Assert.assertTrue(TestHelper.verify(() -> manager.getHandlers().size() == 
handlerCount,
+        SESSION_WAIT_DURATION));
+    Assert.assertTrue(TestHelper.verify(
+        () -> 
manager.getHandlers().stream().allMatch(CallbackHandler::isReady),
+        SESSION_WAIT_DURATION));
 
     // Clean up.
     manager.disconnect();
@@ -644,7 +670,9 @@ public class TestHandleSession extends ZkTestBase {
       resetHandlersStartTime = System.currentTimeMillis();
       try {
         if (!isShutdown) {
-          resetHandlersSemaphore.tryAcquire(20L, TimeUnit.SECONDS);
+          // Safety net only: the test releases the semaphore explicitly. Must 
exceed the
+          // helper thread's polling windows so a slow machine cannot break 
the premise.
+          resetHandlersSemaphore.tryAcquire(2 * SESSION_WAIT_DURATION, 
TimeUnit.MILLISECONDS);
         }
       } catch (InterruptedException ignored) {
         // Ignore the exception.
diff --git 
a/helix-core/src/test/java/org/apache/helix/messaging/handling/TestHelixTaskExecutor.java
 
b/helix-core/src/test/java/org/apache/helix/messaging/handling/TestHelixTaskExecutor.java
index a15620048..97f28b003 100644
--- 
a/helix-core/src/test/java/org/apache/helix/messaging/handling/TestHelixTaskExecutor.java
+++ 
b/helix-core/src/test/java/org/apache/helix/messaging/handling/TestHelixTaskExecutor.java
@@ -906,7 +906,11 @@ public class TestHelixTaskExecutor {
       LOG.info("Created message {} with timeout {} ms", msg.getId(), 
msg.getExecutionTimeout());
     }
     changeContext.setChangeType(HelixConstants.ChangeType.MESSAGE);
-    executor.onMessage("someInstance", msgList, changeContext);
+    // Pass a copy: HelixTaskExecutor.onMessage sorts the list it is given in 
place, newest message
+    // first (Message.CREATE_TIME_COMPARATOR). On a slow machine the 4 
messages can straddle a
+    // millisecond boundary, which would reorder msgList and make the 
index-based assertions below
+    // check the wrong messages.
+    executor.onMessage("someInstance", new ArrayList<>(msgList), 
changeContext);
     LOG.info("Submitted {} messages to executor", nMsgs2);
 
     // Poll until the 2 short-timeout messages have been cancelled (the 
handler sets Cancelcount
@@ -993,7 +997,9 @@ public class TestHelixTaskExecutor {
       msgList.add(msg);
     }
     changeContext.setChangeType(HelixConstants.ChangeType.MESSAGE);
-    executor.onMessage("someInstance", msgList, changeContext);
+    // Pass a copy: onMessage sorts the given list in place (newest create 
time first), which would
+    // reorder msgList on a slow machine and break the index-based assertions 
below.
+    executor.onMessage("someInstance", new ArrayList<>(msgList), 
changeContext);
 
     // All processing, timeouts and retries happen asynchronously on the 
executor threads, so
     // poll until every message has reached its final state and the task map 
is drained.

Reply via email to