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

Reply via email to