This is an automated email from the ASF dual-hosted git repository.

junegunn pushed a commit to branch branch-2.6
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2.6 by this push:
     new 1af586b6528 HBASE-29267 Support shaded clients in Exception 
deserialization by prefixing shaded package in IPCUtil (#6917)
1af586b6528 is described below

commit 1af586b65282731d123cfd93a36dcb9d5a035d68
Author: Minwoo Kang <[email protected]>
AuthorDate: Mon May 18 16:05:31 2026 +0900

    HBASE-29267 Support shaded clients in Exception deserialization by 
prefixing shaded package in IPCUtil (#6917)
    
    Signed-off-by: Junegunn Choi <[email protected]>
---
 .../java/org/apache/hadoop/hbase/ipc/IPCUtil.java  |  13 ++-
 .../apache/hadoop/hbase/ipc/ShadedPrefixUtil.java  | 127 +++++++++++++++++++++
 .../hadoop/hbase/ipc/TestShadedPrefixUtil.java     | 106 +++++++++++++++++
 3 files changed, 241 insertions(+), 5 deletions(-)

diff --git 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/ipc/IPCUtil.java 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/ipc/IPCUtil.java
index 3058f0caee4..e1590da8077 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/ipc/IPCUtil.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/ipc/IPCUtil.java
@@ -151,7 +151,8 @@ class IPCUtil {
    * @return RemoteException made from passed <code>e</code>
    */
   static RemoteException createRemoteException(final ExceptionResponse e) {
-    String innerExceptionClassName = e.getExceptionClassName();
+    String innerExceptionClassName =
+      ShadedPrefixUtil.getInstance().resolveShading(e.getExceptionClassName());
     boolean doNotRetry = e.getDoNotRetry();
     boolean serverOverloaded = e.hasServerOverloaded() && 
e.getServerOverloaded();
     return e.hasHostname() ?
@@ -164,16 +165,18 @@ class IPCUtil {
 
   /** Returns True if the exception is a fatal connection exception. */
   static boolean isFatalConnectionException(ExceptionResponse e) {
-    if 
(e.getExceptionClassName().equals(FatalConnectionException.class.getName())) {
+    String exceptionClassName =
+      ShadedPrefixUtil.getInstance().resolveShading(e.getExceptionClassName());
+    if (FatalConnectionException.class.getName().equals(exceptionClassName)) {
       return true;
     }
     // try our best to check for sub classes of FatalConnectionException
     try {
-      return e.getExceptionClassName() != null && 
FatalConnectionException.class.isAssignableFrom(
-        Class.forName(e.getExceptionClassName(), false, 
IPCUtil.class.getClassLoader()));
+      return exceptionClassName != null && FatalConnectionException.class
+        .isAssignableFrom(Class.forName(exceptionClassName, false, 
IPCUtil.class.getClassLoader()));
       // Class.forName may throw ExceptionInInitializerError so we have to 
catch Throwable here
     } catch (Throwable t) {
-      LOG.debug("Can not get class object for {}", e.getExceptionClassName(), 
t);
+      LOG.debug("Can not get class object for {}", exceptionClassName, t);
       return false;
     }
   }
diff --git 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/ipc/ShadedPrefixUtil.java 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/ipc/ShadedPrefixUtil.java
new file mode 100644
index 00000000000..b132be82d38
--- /dev/null
+++ 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/ipc/ShadedPrefixUtil.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.ipc;
+
+import java.util.concurrent.ConcurrentHashMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class ShadedPrefixUtil {
+  private static final Logger LOG = 
LoggerFactory.getLogger(ShadedPrefixUtil.class);
+
+  // Marked with '!' to prevent shading tools from replacing this value
+  static final String ORIGINAL_BASE = "org!apache!hadoop!hbase".replace('!', 
'.');
+
+  private static final ShadedPrefixUtil INSTANCE =
+    newInstance(ShadedPrefixUtil.class.getPackage().getName());
+
+  /**
+   * The shaded equivalent of "org.apache.hadoop.hbase", or null if not in a 
shaded environment.
+   * <ul>
+   * <li>Prefix-prepend shading (e.g., current package 
"a.b.c.org.apache.hadoop.hbase.ipc"):
+   * "a.b.c.org.apache.hadoop.hbase"</li>
+   * <li>Full-replacement shading (e.g., current package "shaded.hbase1.ipc"): 
"shaded.hbase1"</li>
+   * </ul>
+   */
+  private final String shadedBase;
+
+  /**
+   * Cache from original class name to its resolved (shaded or original) 
equivalent.
+   */
+  private final ConcurrentHashMap<String, String> resolvedClassNames = new 
ConcurrentHashMap<>();
+
+  private ShadedPrefixUtil(String shadedBase) {
+    this.shadedBase = shadedBase;
+  }
+
+  static ShadedPrefixUtil newInstance(String currentPackageName) {
+    // The ipc suffix identifies which sub-package this class lives in
+    final String ipcSuffix = ".ipc";
+    final String originalPackage = ORIGINAL_BASE + ipcSuffix;
+
+    if (currentPackageName == null || 
currentPackageName.equals(originalPackage)) {
+      LOG.debug("{} is not a shaded package", currentPackageName);
+      return new ShadedPrefixUtil(null);
+    }
+
+    if (!currentPackageName.endsWith(ipcSuffix)) {
+      LOG.debug("Cannot determine shading mapping for {}", currentPackageName);
+      return new ShadedPrefixUtil(null);
+    }
+
+    // Works for both prefix-prepend and full-replacement shading strategies:
+    // - "a.b.c.org.apache.hadoop.hbase.ipc" -> shadedBase = 
"a.b.c.org.apache.hadoop.hbase"
+    // - "shaded.hbase1.ipc" -> shadedBase = "shaded.hbase1"
+    String detectedShadedBase =
+      currentPackageName.substring(0, currentPackageName.length() - 
ipcSuffix.length());
+    LOG.debug("{} is a shaded package, shadedBase={}", currentPackageName, 
detectedShadedBase);
+    return new ShadedPrefixUtil(detectedShadedBase);
+  }
+
+  String getShadedBase() {
+    return shadedBase;
+  }
+
+  /**
+   * Maps an original HBase class name to its shaded equivalent by replacing 
the
+   * "org.apache.hadoop.hbase" prefix with the detected shaded base package. 
Returns the original
+   * class name unchanged if not in a shaded environment or if the class name 
does not start with
+   * the original HBase package. Note: this is a pure string transformation 
with no class loading.
+   * Use {@link #resolveShading(String)} when class existence must be verified.
+   */
+  String applyShading(String className) {
+    if (shadedBase == null || className == null) {
+      return className;
+    }
+    if (!className.startsWith(ORIGINAL_BASE + ".")) {
+      return className;
+    }
+    return shadedBase + className.substring(ORIGINAL_BASE.length());
+  }
+
+  /**
+   * Returns the effective class name for the given original HBase class name. 
Prefers the shaded
+   * class name when it can be loaded, and falls back to the original when the 
shaded class is not
+   * available (e.g., when exception classes are intentionally excluded from 
shading). The result is
+   * cached so that Class.forName is invoked at most once per distinct class 
name.
+   */
+  String resolveShading(String originalName) {
+    if (shadedBase == null || originalName == null) {
+      return originalName;
+    }
+    return resolvedClassNames.computeIfAbsent(originalName, 
this::computeResolvedName);
+  }
+
+  private String computeResolvedName(String originalName) {
+    String shadedName = applyShading(originalName);
+    if (shadedName.equals(originalName)) {
+      return originalName;
+    }
+    try {
+      Class.forName(shadedName, false, 
ShadedPrefixUtil.class.getClassLoader());
+      return shadedName;
+    } catch (Throwable t) {
+      LOG.debug("Shaded class {} not found, falling back to {}", shadedName, 
originalName);
+      return originalName;
+    }
+  }
+
+  public static ShadedPrefixUtil getInstance() {
+    return INSTANCE;
+  }
+}
diff --git 
a/hbase-client/src/test/java/org/apache/hadoop/hbase/ipc/TestShadedPrefixUtil.java
 
b/hbase-client/src/test/java/org/apache/hadoop/hbase/ipc/TestShadedPrefixUtil.java
new file mode 100644
index 00000000000..fb691be871b
--- /dev/null
+++ 
b/hbase-client/src/test/java/org/apache/hadoop/hbase/ipc/TestShadedPrefixUtil.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.ipc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.apache.hadoop.hbase.testclassification.ClientTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag(ClientTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestShadedPrefixUtil {
+
+  private static final String EXCEPTION_CLASS =
+    "org.apache.hadoop.hbase.exceptions.RegionMovedException";
+
+  @Test
+  public void testApplyShadingNotShaded() {
+    ShadedPrefixUtil util = ShadedPrefixUtil.getInstance();
+    assertNull(util.getShadedBase());
+    assertEquals(EXCEPTION_CLASS, util.applyShading(EXCEPTION_CLASS));
+    assertNull(util.applyShading(null));
+  }
+
+  @Test
+  public void testApplyShadingPrependPrefix() {
+    ShadedPrefixUtil util =
+      ShadedPrefixUtil.newInstance("a.b.c." + 
ShadedPrefixUtil.class.getPackage().getName());
+    assertEquals("a.b.c.org.apache.hadoop.hbase", util.getShadedBase());
+    assertEquals("a.b.c." + EXCEPTION_CLASS, 
util.applyShading(EXCEPTION_CLASS));
+    assertNull(util.applyShading(null));
+  }
+
+  @Test
+  public void testApplyShadingFullReplacement() {
+    ShadedPrefixUtil util = ShadedPrefixUtil.newInstance("shaded.hbase1.ipc");
+    assertEquals("shaded.hbase1", util.getShadedBase());
+    assertEquals("shaded.hbase1.exceptions.RegionMovedException",
+      util.applyShading(EXCEPTION_CLASS));
+    assertNull(util.applyShading(null));
+  }
+
+  @Test
+  public void testApplyShadingNonHBaseClass() {
+    ShadedPrefixUtil util = ShadedPrefixUtil.newInstance("shaded.hbase1.ipc");
+    assertEquals("com.example.SomeClass", 
util.applyShading("com.example.SomeClass"));
+  }
+
+  @Test
+  public void testApplyShadingNullPackage() {
+    ShadedPrefixUtil util = ShadedPrefixUtil.newInstance(null);
+    assertNull(util.getShadedBase());
+    assertEquals(EXCEPTION_CLASS, util.applyShading(EXCEPTION_CLASS));
+  }
+
+  @Test
+  public void testApplyShadingUnrecognizablePackage() {
+    ShadedPrefixUtil util = 
ShadedPrefixUtil.newInstance("some.unrelated.package");
+    assertNull(util.getShadedBase());
+    assertEquals(EXCEPTION_CLASS, util.applyShading(EXCEPTION_CLASS));
+  }
+
+  @Test
+  public void testResolveShadingNotShaded() {
+    ShadedPrefixUtil util = ShadedPrefixUtil.getInstance();
+    // No shading: returned as-is without class loading
+    assertEquals(EXCEPTION_CLASS, util.resolveShading(EXCEPTION_CLASS));
+    assertNull(util.resolveShading(null));
+  }
+
+  @Test
+  public void testResolveShadingFallsBackWhenShadedClassMissing() {
+    // In the test classpath no "shaded.hbase1.*" classes exist, so 
resolveShading must
+    // fall back to the original name rather than the transformed one.
+    ShadedPrefixUtil util = ShadedPrefixUtil.newInstance("shaded.hbase1.ipc");
+    assertEquals(EXCEPTION_CLASS, util.resolveShading(EXCEPTION_CLASS));
+  }
+
+  @Test
+  public void testResolveShadingCachesResult() {
+    ShadedPrefixUtil util = ShadedPrefixUtil.newInstance("shaded.hbase1.ipc");
+    String first = util.resolveShading(EXCEPTION_CLASS);
+    String second = util.resolveShading(EXCEPTION_CLASS);
+    // Identical object reference confirms the cache returned the memoized 
result
+    assertSame(first, second);
+  }
+}

Reply via email to