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

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


The following commit(s) were added to refs/heads/branch-3 by this push:
     new 86ccf712cb5 HBASE-29706 Modify table with lazy mode should pass if 
coprocessors have not changed (#7575)
86ccf712cb5 is described below

commit 86ccf712cb5f2ff3a4ee54fd58e415d618e67354
Author: Aman Poonia <[email protected]>
AuthorDate: Thu Jan 1 14:33:53 2026 +0530

    HBASE-29706 Modify table with lazy mode should pass if coprocessors have 
not changed (#7575)
    
    Signed-off-by: Duo Zhang <[email protected]>
    Reviewed-by: Umesh Kumar <[email protected]>
    (cherry picked from commit a2177d787ccd8dac0986dfd94f204019a4745161)
---
 .../hbase/client/CoprocessorDescriptorBuilder.java | 35 ++++++++--
 .../master/procedure/ModifyTableProcedure.java     | 26 +++++--
 .../master/procedure/TestModifyTableProcedure.java | 79 ++++++++++++++++++++++
 3 files changed, 131 insertions(+), 9 deletions(-)

diff --git 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/CoprocessorDescriptorBuilder.java
 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/CoprocessorDescriptorBuilder.java
index cb0caca21b0..00d666e38d8 100644
--- 
a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/CoprocessorDescriptorBuilder.java
+++ 
b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/CoprocessorDescriptorBuilder.java
@@ -19,6 +19,7 @@ package org.apache.hadoop.hbase.client;
 
 import java.util.Collections;
 import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.TreeMap;
@@ -42,7 +43,8 @@ public final class CoprocessorDescriptorBuilder {
   private final String className;
   private String jarPath;
   private int priority = Coprocessor.PRIORITY_USER;
-  private Map<String, String> properties = new TreeMap();
+  // HBASE-29706 Fix hashcode calculation. Use NavigableMap instead of Map
+  private NavigableMap<String, String> properties = new TreeMap<>();
 
   public CoprocessorDescriptorBuilder setJarPath(String jarPath) {
     this.jarPath = jarPath;
@@ -76,10 +78,10 @@ public final class CoprocessorDescriptorBuilder {
     private final String className;
     private final String jarPath;
     private final int priority;
-    private final Map<String, String> properties;
+    private final NavigableMap<String, String> properties;
 
     private CoprocessorDescriptorImpl(String className, String jarPath, int 
priority,
-      Map<String, String> properties) {
+      NavigableMap<String, String> properties) {
       this.className = className;
       this.jarPath = jarPath;
       this.priority = priority;
@@ -102,8 +104,8 @@ public final class CoprocessorDescriptorBuilder {
     }
 
     @Override
-    public Map<String, String> getProperties() {
-      return Collections.unmodifiableMap(properties);
+    public NavigableMap<String, String> getProperties() {
+      return Collections.unmodifiableNavigableMap(properties);
     }
 
     @Override
@@ -111,5 +113,28 @@ public final class CoprocessorDescriptorBuilder {
       return "class:" + className + ", jarPath:" + jarPath + ", priority:" + 
priority
         + ", properties:" + properties;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof CoprocessorDescriptor)) {
+        return false;
+      }
+      CoprocessorDescriptor other = (CoprocessorDescriptor) obj;
+      if (
+        priority != other.getPriority() || !Objects.equals(className, 
other.getClassName())
+          || !Objects.equals(getJarPath(), other.getJarPath())
+      ) {
+        return false;
+      }
+      return Objects.equals(getProperties(), other.getProperties());
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(className, jarPath, priority, properties);
+    }
   }
 }
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java
index 0d8981891e5..3450f305910 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/ModifyTableProcedure.java
@@ -19,6 +19,7 @@ package org.apache.hadoop.hbase.master.procedure;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -32,6 +33,7 @@ import org.apache.hadoop.hbase.HBaseIOException;
 import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.TableNotFoundException;
+import org.apache.hadoop.hbase.client.CoprocessorDescriptor;
 import org.apache.hadoop.hbase.client.RegionInfo;
 import org.apache.hadoop.hbase.client.RegionReplicaUtil;
 import org.apache.hadoop.hbase.client.TableDescriptor;
@@ -132,10 +134,7 @@ public class ModifyTableProcedure extends 
AbstractStateMachineTableProcedure<Mod
         throw new HBaseIOException(
           "Cannot add or remove column families when this modification " + 
"won't reopen regions.");
       }
-      if (
-        this.unmodifiedTableDescriptor.getCoprocessorDescriptors().hashCode()
-            != 
this.modifiedTableDescriptor.getCoprocessorDescriptors().hashCode()
-      ) {
+      if (isCoprocModified()) {
         throw new HBaseIOException(
           "Can not modify Coprocessor when table modification won't reopen 
regions");
       }
@@ -152,6 +151,25 @@ public class ModifyTableProcedure extends 
AbstractStateMachineTableProcedure<Mod
     }
   }
 
+  private boolean isCoprocModified() {
+    final Collection<CoprocessorDescriptor> unmodifiedCoprocs =
+      this.unmodifiedTableDescriptor.getCoprocessorDescriptors();
+    final Collection<CoprocessorDescriptor> modifiedCoprocs =
+      this.modifiedTableDescriptor.getCoprocessorDescriptors();
+
+    if (unmodifiedCoprocs.size() != modifiedCoprocs.size()) {
+      return true;
+    }
+
+    final Set<CoprocessorDescriptor> unmodifiedSet = new 
HashSet<>(unmodifiedCoprocs);
+    for (CoprocessorDescriptor cp : modifiedCoprocs) {
+      if (!unmodifiedSet.remove(cp)) {
+        return true;
+      }
+    }
+    return !unmodifiedSet.isEmpty();
+  }
+
   /**
    * Comparing the value associated with a given key across two 
TableDescriptor instances'
    * properties.
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java
index c8043b8ee3b..26b190f798b 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestModifyTableProcedure.java
@@ -35,6 +35,8 @@ import 
org.apache.hadoop.hbase.client.PerClientRandomNonceGenerator;
 import org.apache.hadoop.hbase.client.RegionInfo;
 import org.apache.hadoop.hbase.client.TableDescriptor;
 import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.constraint.ConstraintProcessor;
+import org.apache.hadoop.hbase.coprocessor.SimpleRegionObserver;
 import org.apache.hadoop.hbase.io.compress.Compression;
 import 
org.apache.hadoop.hbase.master.procedure.MasterProcedureTestingUtility.StepHook;
 import org.apache.hadoop.hbase.procedure2.Procedure;
@@ -698,4 +700,81 @@ public class TestModifyTableProcedure extends 
TestTableDDLProcedureBase {
       assertTrue(e.getMessage().contains("Can not modify REGION_REPLICATION"));
     }
   }
+
+  @Test
+  public void testModifyTableWithCoprocessorAndColumnFamilyPropertyChange() 
throws IOException {
+    // HBASE-29706 - This test validates the fix for the bug where modifying 
only column family
+    // properties
+    // (like COMPRESSION) with REOPEN_REGIONS=false would incorrectly throw an 
error when
+    // coprocessors are present. The bug was caused by comparing collection 
hash codes
+    // instead of actual descriptor content, which failed when HashMap 
iteration order varied.
+
+    final boolean reopenRegions = false;
+    final TableName tableName = TableName.valueOf(name.getMethodName());
+    final ProcedureExecutor<MasterProcedureEnv> procExec = 
getMasterProcedureExecutor();
+
+    MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf");
+
+    // Step 1: Add coprocessors to the table
+    TableDescriptor htd = UTIL.getAdmin().getDescriptor(tableName);
+    final String cp2 = ConstraintProcessor.class.getName();
+    TableDescriptor descriptorWithCoprocessor = 
TableDescriptorBuilder.newBuilder(htd)
+      
.setCoprocessor(CoprocessorDescriptorBuilder.newBuilder(SimpleRegionObserver.class.getName())
+        .setPriority(100).build())
+      
.setCoprocessor(CoprocessorDescriptorBuilder.newBuilder(cp2).setPriority(200).build())
+      .build();
+    long procId = ProcedureTestingUtility.submitAndWait(procExec, new 
ModifyTableProcedure(
+      procExec.getEnvironment(), descriptorWithCoprocessor, null, htd, false, 
true));
+    ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId));
+
+    // Verify coprocessors were added
+    TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName);
+    assertEquals(2, currentHtd.getCoprocessorDescriptors().size());
+    assertTrue("First coprocessor should be present",
+      currentHtd.hasCoprocessor(SimpleRegionObserver.class.getName()));
+    assertTrue("Second coprocessor should be present", 
currentHtd.hasCoprocessor(cp2));
+
+    // Step 2: Modify only the column family property (COMPRESSION) with 
REOPEN_REGIONS=false
+    // This should SUCCEED because we're not actually modifying the 
coprocessor,
+    // just the column family compression setting.
+    htd = UTIL.getAdmin().getDescriptor(tableName);
+    TableDescriptor modifiedDescriptor =
+      TableDescriptorBuilder
+        .newBuilder(htd).modifyColumnFamily(ColumnFamilyDescriptorBuilder
+          
.newBuilder("cf".getBytes()).setCompressionType(Compression.Algorithm.SNAPPY).build())
+        .build();
+
+    // This should NOT throw an error - the fix ensures order-independent 
coprocessor comparison
+    long procId2 = ProcedureTestingUtility.submitAndWait(procExec, new 
ModifyTableProcedure(
+      procExec.getEnvironment(), modifiedDescriptor, null, htd, false, 
reopenRegions));
+    ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId2));
+
+    // Verify the modification succeeded
+    currentHtd = UTIL.getAdmin().getDescriptor(tableName);
+    assertEquals("Coprocessors should still be present", 2,
+      currentHtd.getCoprocessorDescriptors().size());
+    assertTrue("First coprocessor should still be present",
+      currentHtd.hasCoprocessor(SimpleRegionObserver.class.getName()));
+    assertTrue("Second coprocessor should still be present", 
currentHtd.hasCoprocessor(cp2));
+    assertEquals("Compression should be updated in table descriptor", 
Compression.Algorithm.SNAPPY,
+      currentHtd.getColumnFamily("cf".getBytes()).getCompressionType());
+
+    // Verify regions haven't picked up the change yet (since 
reopenRegions=false)
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      assertEquals("Regions should still have old compression", 
Compression.Algorithm.NONE,
+        
r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType());
+    }
+
+    // Force regions to reopen
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      getMaster().getAssignmentManager().move(r.getRegionInfo());
+    }
+
+    // After reopen, regions should have the new compression setting
+    for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) {
+      assertEquals("Regions should now have new compression after reopen",
+        Compression.Algorithm.SNAPPY,
+        
r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType());
+    }
+  }
 }

Reply via email to