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);
+ }
+}