This is an automated email from the ASF dual-hosted git repository.
meszibalu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/master by this push:
new e3da929fc0a HBASE-30160 Prevent region creation if the encoded region
names are the same (#8395)
e3da929fc0a is described below
commit e3da929fc0a197e7a8a7a50bdbe574f4124003eb
Author: Dev Hingu <[email protected]>
AuthorDate: Wed Jul 8 19:53:06 2026 +0530
HBASE-30160 Prevent region creation if the encoded region names are the
same (#8395)
Signed-off-by: Balazs Meszaros <[email protected]>
---
.../hbase/master/assignment/RegionStates.java | 11 +-
.../assignment/SplitTableRegionProcedure.java | 3 +
.../master/procedure/CreateTableProcedure.java | 8 ++
.../hadoop/hbase/util/ModifyRegionUtils.java | 28 +++++
.../TestEncodedNameCollisionDetection.java | 125 +++++++++++++++++++++
5 files changed, 173 insertions(+), 2 deletions(-)
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/RegionStates.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/RegionStates.java
index b33c6c49fdd..731fba3b442 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/RegionStates.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/RegionStates.java
@@ -330,6 +330,14 @@ public class RegionStates {
regionNode -> !regionNode.isInState(State.SPLIT) &&
!regionNode.getRegionInfo().isSplit());
}
+ /**
+ * Returns all regions of the table, irrespective of assignment or
split/offline state.
+ */
+ public List<RegionInfo> getAllRegionsOfTable(TableName table) {
+ return
getTableRegionStateNodes(table).stream().map(RegionStateNode::getRegionInfo)
+ .collect(Collectors.toList());
+ }
+
/**
* Get the regions for deleting a table.
* <p/>
@@ -338,8 +346,7 @@ public class RegionStates {
* references to the regions, we will lose the data of the regions.
*/
public List<RegionInfo> getRegionsOfTableForDeleting(TableName table) {
- return
getTableRegionStateNodes(table).stream().map(RegionStateNode::getRegionInfo)
- .collect(Collectors.toList());
+ return getAllRegionsOfTable(table);
}
/** Returns Return the regions of the table and filter them. */
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/SplitTableRegionProcedure.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/SplitTableRegionProcedure.java
index 690a4e1d5d6..e601006c43a 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/SplitTableRegionProcedure.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/assignment/SplitTableRegionProcedure.java
@@ -71,6 +71,7 @@ import
org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFac
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.CommonFSUtils;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
+import org.apache.hadoop.hbase.util.ModifyRegionUtils;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.hadoop.hbase.util.Threads;
import org.apache.hadoop.hbase.wal.WALSplitUtil;
@@ -141,6 +142,8 @@ public class SplitTableRegionProcedure
.setEndKey(bestSplitRow).setSplit(false).setRegionId(rid).build();
this.daughterTwoRI =
RegionInfoBuilder.newBuilder(table).setStartKey(bestSplitRow)
.setEndKey(regionToSplit.getEndKey()).setSplit(false).setRegionId(rid).build();
+
ModifyRegionUtils.checkForEncodedNameCollisions(Arrays.asList(daughterOneRI,
daughterTwoRI),
+ env.getAssignmentManager().getRegionStates());
if (tableDescriptor.getRegionSplitPolicyClassName() != null) {
// Since we don't have region reference here, creating the split policy
instance without it.
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/CreateTableProcedure.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/CreateTableProcedure.java
index 6f7ab6b8210..26357c3b373 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/CreateTableProcedure.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/CreateTableProcedure.java
@@ -296,6 +296,14 @@ public class CreateTableProcedure extends
AbstractStateMachineTableProcedure<Cre
return false;
}
+ try {
+ ModifyRegionUtils.checkForEncodedNameCollisions(newRegions,
+ env.getAssignmentManager().getRegionStates());
+ } catch (DoNotRetryIOException e) {
+ setFailure("master-create-table", e);
+ return false;
+ }
+
if (!tableName.isSystemTable()) {
// do not check rs group for system tables as we may block the bootstrap.
Supplier<String> forWhom = () -> "table " + tableName;
diff --git
a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java
b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java
index d91cd9b7861..72c13a8b7b3 100644
---
a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java
+++
b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ModifyRegionUtils.java
@@ -21,7 +21,10 @@ import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
@@ -30,10 +33,12 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.DoNotRetryIOException;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.RegionInfoBuilder;
import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.master.assignment.RegionStates;
import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
import org.apache.hadoop.hbase.regionserver.HRegion;
import org.apache.yetus.audience.InterfaceAudience;
@@ -82,6 +87,29 @@ public abstract class ModifyRegionUtils {
return hRegionInfos;
}
+ /**
+ * Checks candidate regions for encoded\-name collisions. Ensures there are
no duplicates in the
+ * input and no conflicts with existing region states.
+ */
+ public static void checkForEncodedNameCollisions(final
Collection<RegionInfo> candidates,
+ final RegionStates regionStates) throws IOException {
+ if (candidates == null || candidates.isEmpty()) {
+ return;
+ }
+ Objects.requireNonNull(regionStates, "regionStates is null");
+ Set<String> candidateNames = new HashSet<>();
+ for (RegionInfo ri : candidates) {
+ String encoded = ri.getEncodedName();
+ if (
+ !candidateNames.add(encoded)
+ || regionStates.getRegionStateNodeFromEncodedRegionName(encoded) !=
null
+ ) {
+ throw new DoNotRetryIOException("Encoded region name collision
detected: '" + encoded
+ + "' for table " + ri.getTable() + ". Refusing to proceed.");
+ }
+ }
+ }
+
/**
* Create new set of regions on the specified file-system. NOTE: that you
should add the regions
* to hbase:meta after this operation.
diff --git
a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestEncodedNameCollisionDetection.java
b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestEncodedNameCollisionDetection.java
new file mode 100644
index 00000000000..dcb6d953eed
--- /dev/null
+++
b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestEncodedNameCollisionDetection.java
@@ -0,0 +1,125 @@
+/*
+ * 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.master.procedure;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.hadoop.hbase.DoNotRetryIOException;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.client.RegionInfoBuilder;
+import org.apache.hadoop.hbase.master.assignment.RegionStateNode;
+import org.apache.hadoop.hbase.master.assignment.RegionStates;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.util.ModifyRegionUtils;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for encoded region-name collision detection (HBASE-30160). If two
regions end up with the
+ * same encoded name, we should fail fast instead of allowing subtle metadata
corruption later.
+ */
+
+@Tag(SmallTests.TAG)
+public class TestEncodedNameCollisionDetection {
+
+ /**
+ * Happy-path check: distinct candidate regions should pass without throwing.
+ */
+ @Test
+ public void testAcceptsDistinctCandidates() {
+ TableName tableName = TableName.valueOf("test_table");
+ long regionId = System.currentTimeMillis();
+
+ RegionInfo ri1 = RegionInfoBuilder.newBuilder(tableName).setStartKey(new
byte[] { 0, 0 })
+ .setEndKey(new byte[] { 1, 0
}).setSplit(false).setRegionId(regionId).build();
+
+ RegionInfo ri2 = RegionInfoBuilder.newBuilder(tableName).setStartKey(new
byte[] { 1, 0 })
+ .setEndKey(new byte[] { 2, 0 }).setSplit(false).setRegionId(regionId +
1).build();
+
+ RegionStates regionStates = mock(RegionStates.class);
+
+ assertDoesNotThrow(
+ () -> ModifyRegionUtils.checkForEncodedNameCollisions(Arrays.asList(ri1,
ri2), regionStates));
+ }
+
+ /**
+ * Verifies that duplicate encoded region names within candidate regions are
rejected.
+ */
+ @Test
+ public void testDetectsDuplicatesInCandidates() {
+ TableName tableName = TableName.valueOf("test_table");
+ RegionInfo ri1 = mockRegionInfo(tableName, "same-encoded-name");
+ RegionInfo ri2 = mockRegionInfo(tableName, "same-encoded-name");
+
+ RegionStates regionStates = mock(RegionStates.class);
+
+ DoNotRetryIOException exception = assertThrows(DoNotRetryIOException.class,
+ () -> ModifyRegionUtils.checkForEncodedNameCollisions(Arrays.asList(ri1,
ri2), regionStates));
+ assertTrue(exception.getMessage().contains("Encoded region name collision
detected"));
+ }
+
+ /**
+ * A candidate region should be rejected if its encoded name already exists.
+ */
+ @Test
+ public void testDetectsCollisionWithExistingRegions() {
+ TableName tableName = TableName.valueOf("test_table");
+ RegionInfo candidateRegion = mockRegionInfo(tableName,
"same-encoded-name");
+ RegionStates regionStates = mock(RegionStates.class);
+
when(regionStates.getRegionStateNodeFromEncodedRegionName("same-encoded-name"))
+ .thenReturn(mock(RegionStateNode.class));
+
+ DoNotRetryIOException exception =
+ assertThrows(DoNotRetryIOException.class, () -> ModifyRegionUtils
+ .checkForEncodedNameCollisions(Arrays.asList(candidateRegion),
regionStates));
+ assertTrue(exception.getMessage().contains("Encoded region name collision
detected"));
+ }
+
+ /**
+ * Test that checkForEncodedNameCollisions handles empty/null inputs and
rejects null RegionStates
+ * when candidates are present.
+ */
+ @Test
+ public void testInputValidationAndNullRegionStatesBehavior() {
+ assertDoesNotThrow(() ->
ModifyRegionUtils.checkForEncodedNameCollisions(null, null));
+ assertDoesNotThrow(
+ () ->
ModifyRegionUtils.checkForEncodedNameCollisions(Collections.emptyList(), null));
+ assertDoesNotThrow(
+ () -> ModifyRegionUtils.checkForEncodedNameCollisions(null,
mock(RegionStates.class)));
+ assertDoesNotThrow(() -> ModifyRegionUtils
+ .checkForEncodedNameCollisions(Collections.emptyList(),
mock(RegionStates.class)));
+
+ RegionInfo candidateRegion =
mockRegionInfo(TableName.valueOf("test_table"), "candidate");
+ assertThrows(NullPointerException.class,
+ () ->
ModifyRegionUtils.checkForEncodedNameCollisions(Arrays.asList(candidateRegion),
null));
+ }
+
+ private RegionInfo mockRegionInfo(TableName tableName, String encodedName) {
+ RegionInfo regionInfo = mock(RegionInfo.class);
+ when(regionInfo.getEncodedName()).thenReturn(encodedName);
+ when(regionInfo.getTable()).thenReturn(tableName);
+ return regionInfo;
+ }
+}