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 653e16e9b Handle unknown ZooKeeper return codes and fix tests broken 
by 3.9.3 (#3203, #3204, #3205, #3206)
653e16e9b is described below

commit 653e16e9bfac7cb5cb103f44cf1ca3941009e0ff
Author: Junkai Xue <[email protected]>
AuthorDate: Fri Nov 6 09:48:44 2026 -0800

    Handle unknown ZooKeeper return codes and fix tests broken by 3.9.3 (#3203, 
#3204, #3205, #3206)
    
    The ZooKeeper 3.9.3 upgrade (#3093) made four CI tests fail.
    
    testHelixDataAccessorReadData exposed a product-side gap. Helix interprets
    async return codes with KeeperException.Code.get(rc) in ZkAsyncCallbacks,
    ZkBaseDataAccessor, ZkCacheBaseDataAccessor and ZkClient. Before 3.9 that
    lookup returned null for a code the client does not know and Helix treated
    the null as a failed operation (ZkAsyncCallbacks even caught the resulting
    NullPointerException on purpose to skip retrying). Since ZooKeeper 3.9
    (ZOOKEEPER-4377) the lookup throws IllegalArgumentException, which turned
    "unknown code means the operation failed" into an exception escaping from
    the callback, and in ZkBaseDataAccessor.get() into a
    HelixMetaDataAccessException even when the caller asked not to throw. All
    those sites now go through ZkAsyncCallbacks.getCode(), which maps a code 
this
    client version does not know (possible with a newer server) to SYSTEMERROR
    with a warning, so it is handled like any other failed, non-retryable
    operation. The needRetry() implementations no longer need their
    catch-everything blocks. MockZkClient, the only producer of such a code,
    faked a failed read with return code 4, which was never a ZooKeeper code; it
    now uses CONNECTIONLOSS.
    
    The three TestZkMetaClient listener tests failed in meta-client's
    TestUtil.getZkWatch, the same reflection helper that was already updated in
    helix-core and zookeeper-api: ZooKeeper 3.7 moved the client watch registry
    from the ZooKeeper#watchManager field behind the package-private
    getWatchManager() accessor.
    
    Tests run locally against 3.9.3: TestZkMetaClient; TestHelixDataAccessor,
    TestZKWatch, TestZkBaseDataAccessor, TestZkCacheAsyncOpSingleThread,
    TestWtCacheAsyncOpSingleThread, TestZkHelixAdmin and TestZkBasis in
    helix-core; TestZkClientAsyncRetry and TestZooKeeperConnection in
    zookeeper-api.
---
 .../helix/manager/zk/ZkBaseDataAccessor.java       | 26 ++++++------
 .../helix/manager/zk/ZkCacheBaseDataAccessor.java  |  2 +-
 .../java/org/apache/helix/mock/MockZkClient.java   |  5 ++-
 .../apache/helix/metaclient/impl/zk/TestUtil.java  | 28 +++++++------
 .../apache/helix/zookeeper/zkclient/ZkClient.java  | 10 ++---
 .../zkclient/callback/ZkAsyncCallbacks.java        | 47 +++++++++++++---------
 6 files changed, 66 insertions(+), 52 deletions(-)

diff --git 
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java 
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java
index 63a5d95d2..0816294b0 100644
--- 
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java
+++ 
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkBaseDataAccessor.java
@@ -639,17 +639,17 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
         }
 
         ZkAsyncCallbacks.GetDataCallbackHandler cb = cbList[i];
-        if (Code.get(cb.getRc()) == Code.OK) {
+        if (ZkAsyncCallbacks.getCode(cb.getRc()) == Code.OK) {
           @SuppressWarnings("unchecked")
           T record = (T) _zkClient.deserialize(cb._data, paths.get(i));
           records.set(i, record);
           if (stats != null) {
             stats.set(i, cb._stat);
           }
-        } else if (Code.get(cb.getRc()) != Code.NONODE && throwException) {
+        } else if (ZkAsyncCallbacks.getCode(cb.getRc()) != Code.NONODE && 
throwException) {
           throw new HelixMetaDataAccessException(String
               .format("Failed to read node %s, return code: %s", paths.get(i),
-                  Code.get(cb.getRc())));
+                  ZkAsyncCallbacks.getCode(cb.getRc())));
         } else {
           pathFailToRead.put(paths.get(i), cb.getRc());
         }
@@ -889,7 +889,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
         cb.waitForSuccess();
         String path = paths.get(i);
 
-        if (Code.get(cb.getRc()) == Code.NONODE) {
+        if (ZkAsyncCallbacks.getCode(cb.getRc()) == Code.NONODE) {
           String parentPath = HelixUtil.getZkParentPath(path);
           parentPaths.set(i, parentPath);
           failOnNoNode = true;
@@ -899,7 +899,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
           needCreate[i] = false;
 
           // if succeeds, record what paths we've created
-          if (Code.get(cb.getRc()) == Code.OK && pathsCreated != null) {
+          if (ZkAsyncCallbacks.getCode(cb.getRc()) == Code.OK && pathsCreated 
!= null) {
             if (pathsCreated.get(i) == null) {
               pathsCreated.set(i, new ArrayList<String>());
             }
@@ -927,7 +927,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
             continue;
           }
 
-          Code rc = Code.get(parentCb.getRc());
+          Code rc = ZkAsyncCallbacks.getCode(parentCb.getRc());
 
           // if parent is created, retry create child
           if (rc == Code.OK || rc == Code.NODEEXISTS) {
@@ -977,7 +977,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
 
       for (int i = 0; i < cbList.length; i++) {
         ZkAsyncCallbacks.CreateCallbackHandler cb = cbList[i];
-        success[i] = (Code.get(cb.getRc()) == Code.OK);
+        success[i] = (ZkAsyncCallbacks.getCode(cb.getRc()) == Code.OK);
       }
 
       return success;
@@ -1052,7 +1052,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
         for (int i = 0; i < cbList.length; i++) {
           ZkAsyncCallbacks.SetDataCallbackHandler cb = cbList[i];
           cb.waitForSuccess();
-          Code rc = Code.get(cb.getRc());
+          Code rc = ZkAsyncCallbacks.getCode(cb.getRc());
           switch (rc) {
             case OK:
               setStats.set(i, cb.getStat());
@@ -1079,7 +1079,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
               continue;
             }
 
-            Code rc = Code.get(createCb.getRc());
+            Code rc = ZkAsyncCallbacks.getCode(createCb.getRc());
             switch (rc) {
               case OK:
                 setStats.set(i, ZNode.ZERO_STAT);
@@ -1102,12 +1102,12 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
       for (int i = 0; i < cbList.length; i++) {
         ZkAsyncCallbacks.SetDataCallbackHandler cb = cbList[i];
 
-        Code rc = Code.get(cb.getRc());
+        Code rc = ZkAsyncCallbacks.getCode(cb.getRc());
         if (rc == Code.OK) {
           success[i] = true;
         } else if (rc == Code.NONODE) {
           ZkAsyncCallbacks.CreateCallbackHandler createCb = createCbList[i];
-          if (Code.get(createCb.getRc()) == Code.OK) {
+          if (ZkAsyncCallbacks.getCode(createCb.getRc()) == Code.OK) {
             success[i] = true;
           }
         }
@@ -1229,7 +1229,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
 
           cb.waitForSuccess();
 
-          switch (Code.get(cb.getRc())) {
+          switch (ZkAsyncCallbacks.getCode(cb.getRc())) {
             case OK:
               updateData.set(i, newDataList.get(i));
               setStats.set(i, cb.getStat());
@@ -1259,7 +1259,7 @@ public class ZkBaseDataAccessor<T> implements 
BaseDataAccessor<T> {
               continue;
             }
 
-            switch (Code.get(createCb.getRc())) {
+            switch (ZkAsyncCallbacks.getCode(createCb.getRc())) {
               case OK:
                 needUpdate[i] = false;
                 updateData.set(i, newDataList.get(i));
diff --git 
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
 
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
index 9691c365d..66b6c84a7 100644
--- 
a/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
+++ 
b/helix-core/src/main/java/org/apache/helix/manager/zk/ZkCacheBaseDataAccessor.java
@@ -483,7 +483,7 @@ public class ZkCacheBaseDataAccessor<T> implements 
HelixPropertyStore<T> {
         boolean[] success = new boolean[size];
         for (int i = 0; i < size; i++) {
           ZkAsyncCallbacks.CreateCallbackHandler cb = createCbList[i];
-          success[i] = (Code.get(cb.getRc()) == Code.OK);
+          success[i] = (ZkAsyncCallbacks.getCode(cb.getRc()) == Code.OK);
 
           updateCache(cache, pathsCreatedList.get(i), success[i], 
serverPaths.get(i),
               records.get(i), ZNode.ZERO_STAT);
diff --git a/helix-core/src/test/java/org/apache/helix/mock/MockZkClient.java 
b/helix-core/src/test/java/org/apache/helix/mock/MockZkClient.java
index 6b834d287..53118db3f 100644
--- a/helix-core/src/test/java/org/apache/helix/mock/MockZkClient.java
+++ b/helix-core/src/test/java/org/apache/helix/mock/MockZkClient.java
@@ -27,6 +27,7 @@ import org.apache.helix.manager.zk.ZNRecordSerializer;
 import org.apache.helix.manager.zk.ZkAsyncCallbacks;
 import org.apache.helix.zookeeper.impl.client.ZkClient;
 import org.apache.helix.zookeeper.api.client.HelixZkClient;
+import org.apache.zookeeper.KeeperException;
 
 public class MockZkClient extends ZkClient implements HelixZkClient {
   Map<String, byte[]> _dataMap;
@@ -42,7 +43,9 @@ public class MockZkClient extends ZkClient implements 
HelixZkClient {
       final ZkAsyncCallbacks.GetDataCallbackHandler cb) {
     if (_dataMap.containsKey(path)) {
       if (_dataMap.get(path) == null) {
-        cb.processResult(4, path, null, _dataMap.get(path), null);
+        // Simulate a failed read with a real ZooKeeper error code. ZooKeeper 
3.9+ rejects
+        // unknown codes in KeeperException.Code.get() instead of returning 
null.
+        cb.processResult(KeeperException.Code.CONNECTIONLOSS.intValue(), path, 
null, null, null);
       } else {
         cb.processResult(0, path, null, _dataMap.get(path), null);
       }
diff --git 
a/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestUtil.java 
b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestUtil.java
index a0aca15e8..4f9d0c1f1 100644
--- 
a/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestUtil.java
+++ 
b/meta-client/src/test/java/org/apache/helix/metaclient/impl/zk/TestUtil.java
@@ -65,34 +65,36 @@ public class TestUtil {
     ZkConnection connection = ((ZkConnection) zkClient.getConnection());
     ZooKeeper zk = connection.getZookeeper();
 
-    java.lang.reflect.Field field = getField(zk.getClass(), "watchManager");
-    field.setAccessible(true);
-    Object watchManager = field.get(zk);
+    // Since ZooKeeper 3.7 the client watch manager lives in ClientCnxn and is 
reached through the
+    // package-private ZooKeeper#getWatchManager() instead of a field.
+    java.lang.reflect.Method getWatchManager = 
ZooKeeper.class.getDeclaredMethod("getWatchManager");
+    getWatchManager.setAccessible(true);
+    Object watchManager = getWatchManager.invoke(zk);
 
     java.lang.reflect.Field field2 = getField(watchManager.getClass(), 
"dataWatches");
     field2.setAccessible(true);
-    HashMap<String, Set<Watcher>> dataWatches =
-        (HashMap<String, Set<Watcher>>) field2.get(watchManager);
+    Map<String, Set<Watcher>> dataWatches =
+        (Map<String, Set<Watcher>>) field2.get(watchManager);
 
     field2 = getField(watchManager.getClass(), "existWatches");
     field2.setAccessible(true);
-    HashMap<String, Set<Watcher>> existWatches =
-        (HashMap<String, Set<Watcher>>) field2.get(watchManager);
+    Map<String, Set<Watcher>> existWatches =
+        (Map<String, Set<Watcher>>) field2.get(watchManager);
 
     field2 = getField(watchManager.getClass(), "childWatches");
     field2.setAccessible(true);
-    HashMap<String, Set<Watcher>> childWatches =
-        (HashMap<String, Set<Watcher>>) field2.get(watchManager);
+    Map<String, Set<Watcher>> childWatches =
+        (Map<String, Set<Watcher>>) field2.get(watchManager);
 
     field2 = getField(watchManager.getClass(), "persistentWatches");
     field2.setAccessible(true);
-    HashMap<String, Set<Watcher>> persistentWatches =
-        (HashMap<String, Set<Watcher>>) field2.get(watchManager);
+    Map<String, Set<Watcher>> persistentWatches =
+        (Map<String, Set<Watcher>>) field2.get(watchManager);
 
     field2 = getField(watchManager.getClass(), "persistentRecursiveWatches");
     field2.setAccessible(true);
-    HashMap<String, Set<Watcher>> persistentRecursiveWatches =
-        (HashMap<String, Set<Watcher>>) field2.get(watchManager);
+    Map<String, Set<Watcher>> persistentRecursiveWatches =
+        (Map<String, Set<Watcher>>) field2.get(watchManager);
 
 
     lists.put("dataWatches", new ArrayList<>(dataWatches.keySet()));
diff --git 
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java 
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java
index 95ca167be..50703a7e0 100644
--- 
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java
+++ 
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/ZkClient.java
@@ -1730,7 +1730,7 @@ public class ZkClient implements Watcher {
 
     callbackHandler.waitForSuccess();
 
-    KeeperException.Code code = 
KeeperException.Code.get(callbackHandler.getRc());
+    KeeperException.Code code = 
ZkAsyncCallbacks.getCode(callbackHandler.getRc());
     if (code == KeeperException.Code.OK) {
       LOG.info("zkclient {}, sycnOnNewSession with sessionID {} async return 
code: {} and proceeds",
           _uid, sessionId, code);
@@ -1913,7 +1913,7 @@ public class ZkClient implements Watcher {
     for (int i = 0; i < opResults.size(); i++) {
       if (opResults.get(i) instanceof OpResult.ErrorResult) {
         failedPathsMap.put(ops.get(i).getPath(),
-            KeeperException.Code.get(((OpResult.ErrorResult) 
opResults.get(i)).getErr()));
+            ZkAsyncCallbacks.getCode(((OpResult.ErrorResult) 
opResults.get(i)).getErr()));
       }
     }
 
@@ -2221,7 +2221,7 @@ public class ZkClient implements Watcher {
 
     /** The exception to throw for a non-OK result code. Mirrors ZooKeeper's 
synchronous API. */
     KeeperException failure(String path) {
-      return KeeperException.create(KeeperException.Code.get(_rc), path);
+      return KeeperException.create(ZkAsyncCallbacks.getCode(_rc), path);
     }
   }
 
@@ -2286,7 +2286,7 @@ public class ZkClient implements Watcher {
         throw e;
       }
       LOG.info("Caller interrupted while a write to {} was in flight; waited 
for its outcome: {}",
-          path, KeeperException.Code.get(write._rc));
+          path, ZkAsyncCallbacks.getCode(write._rc));
     }
     if (write._rc != KeeperException.Code.OK.intValue()) {
       throw write.failure(path);
@@ -3147,7 +3147,7 @@ public class ZkClient implements Watcher {
       InFlightWrite<List<OpResult>> write = new 
InFlightWrite<List<OpResult>>() {
         @Override
         KeeperException failure(String path) {
-          return KeeperException.create(KeeperException.Code.get(_rc));
+          return KeeperException.create(ZkAsyncCallbacks.getCode(_rc));
         }
       };
       return syncWrite(null, currentZookeeper(), write, (zk, w) -> 
zk.multi(ops,
diff --git 
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java
 
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java
index 61c97c231..2b2cb64dd 100644
--- 
a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java
+++ 
b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/zkclient/callback/ZkAsyncCallbacks.java
@@ -37,6 +37,26 @@ import org.slf4j.LoggerFactory;
 public class ZkAsyncCallbacks {
   private static Logger LOG = LoggerFactory.getLogger(ZkAsyncCallbacks.class);
 
+  /**
+   * Translates an async return code into a {@link Code}. A code this client 
version does not know
+   * (possible with a newer server; KeeperException.Code#get returned null for 
it before ZooKeeper
+   * 3.9 and throws IllegalArgumentException since) is reported as {@link 
Code#SYSTEMERROR}, so
+   * callers treat it as a failed, non-retryable operation instead of failing 
on the lookup itself.
+   */
+  public static Code getCode(int rc) {
+    Code code = null;
+    try {
+      code = Code.get(rc);
+    } catch (IllegalArgumentException e) {
+      // handled below together with the pre-3.9 null result
+    }
+    if (code == null) {
+      LOG.warn("Unknown ZooKeeper return code {}, treating it as {}", rc, 
Code.SYSTEMERROR);
+      return Code.SYSTEMERROR;
+    }
+    return code;
+  }
+
   public static class GetDataCallbackHandler extends DefaultCallback 
implements DataCallback {
     public byte[] _data;
     public Stat _stat;
@@ -173,18 +193,12 @@ public class ZkAsyncCallbacks {
 
     @Override
     protected boolean needRetry(int rc) {
-      try {
-        switch (KeeperException.Code.get(rc)) {
-          /** Connection to the server has been lost */
-          case CONNECTIONLOSS:
-            return true;
-          default:
-            return false;
-        }
-      } catch (ClassCastException | NullPointerException ex) {
-        LOG.error("Session {} failed to handle unknown return code {}. Skip 
retrying. ex {}",
-            _sessionId, rc, ex);
-        return false;
+      switch (getCode(rc)) {
+        /** Connection to the server has been lost */
+        case CONNECTIONLOSS:
+          return true;
+        default:
+          return false;
       }
     }
   }
@@ -198,7 +212,7 @@ public class ZkAsyncCallbacks {
 
     public void callback(int rc, String path, Object ctx) {
       if (rc != 0) {
-        LOG.debug(" {}, rc:{}, path {}", this, Code.get(rc), path);
+        LOG.debug(" {}, rc:{} ({}), path {}", this, rc, getCode(rc), path);
       }
 
       if (ctx != null && ctx instanceof ZkAsyncCallMonitorContext) {
@@ -304,8 +318,7 @@ public class ZkAsyncCallbacks {
      * @return true if the error is transient and the operation may succeed 
when being retried.
      */
     protected boolean needRetry(int rc) {
-      try {
-        switch (Code.get(rc)) {
+      switch (getCode(rc)) {
         /** Connection to the server has been lost */
         case CONNECTIONLOSS:
           /** The session has been expired by the server */
@@ -315,10 +328,6 @@ public class ZkAsyncCallbacks {
           return true;
         default:
           return false;
-        }
-      } catch (ClassCastException | NullPointerException ex) {
-        LOG.error("Failed to handle unknown return code {}. Skip retrying.", 
rc, ex);
-        return false;
       }
     }
   }

Reply via email to