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

wchevreuil 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 28808cc4f14 HBASE-30301: Flush command hangs instead of failing for 
read-only clusters (#8503)
28808cc4f14 is described below

commit 28808cc4f14c737b48c74cafd9c83e674bd1a0c3
Author: Kevin Geiszler <[email protected]>
AuthorDate: Thu Jul 30 12:29:58 2026 -0400

    HBASE-30301: Flush command hangs instead of failing for read-only clusters 
(#8503)
    
    Co-authored-by: Claude Code Opus 4.6 <[email protected]>
    
    Signed-off-by: Wellington Chevreuil <[email protected]>
    Signed-off-by: Tak Lon (Stephen) Wu <[email protected]>
---
 .../security/access/MasterReadOnlyController.java  |   7 ++
 .../hbase/security/access/SecureTestUtil.java      |  31 +++++
 .../security/access/TestReadOnlyController.java    |  26 +---
 .../access/TestReadOnlyControllerFlush.java        | 134 +++++++++++++++++++++
 .../TestReadOnlyControllerMasterObserver.java      |   7 ++
 5 files changed, 181 insertions(+), 24 deletions(-)

diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
index c2eb8833b08..e6efcac856e 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/MasterReadOnlyController.java
@@ -262,6 +262,13 @@ public class MasterReadOnlyController extends 
AbstractReadOnlyController
     MasterObserver.super.preMasterStoreFlush(ctx);
   }
 
+  @Override
+  public void preTableFlush(final 
ObserverContext<MasterCoprocessorEnvironment> ctx,
+    final TableName tableName) throws IOException {
+    internalReadOnlyGuard();
+    MasterObserver.super.preTableFlush(ctx, tableName);
+  }
+
   @Override
   public void preSetUserQuota(ObserverContext<MasterCoprocessorEnvironment> 
ctx, String userName,
     GlobalQuotaSettings quotas) throws IOException {
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java
index 89ea13f0ec5..5c54fb42fa3 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java
@@ -32,6 +32,7 @@ import java.util.concurrent.CountDownLatch;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.Coprocessor;
 import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.NamespaceDescriptor;
 import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
 import org.apache.hadoop.hbase.TableName;
@@ -53,9 +54,12 @@ import org.apache.hadoop.hbase.coprocessor.MasterObserver;
 import org.apache.hadoop.hbase.coprocessor.ObserverContext;
 import org.apache.hadoop.hbase.io.hfile.HFile;
 import org.apache.hadoop.hbase.ipc.RemoteWithExtrasException;
+import org.apache.hadoop.hbase.master.HMaster;
 import org.apache.hadoop.hbase.regionserver.HRegion;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
 import org.apache.hadoop.hbase.security.AccessDeniedException;
 import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.hbase.util.ConfigurationUtil;
 import org.apache.hadoop.hbase.util.JVMClusterUtil.RegionServerThread;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -860,4 +864,31 @@ public class SecureTestUtil {
       }
     }
   }
+
+  public static void enableReadOnlyMode(Configuration conf, HMaster hMaster,
+    HRegionServer hRegionServer) {
+    if (!ConfigurationUtil.isReadOnlyModeEnabledInConf(conf)) {
+      LOG.info("Dynamically enabling Read-Only mode by setting {} to true",
+        HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY);
+      conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+      notifyReadOnlyObservers(conf, hMaster, hRegionServer);
+    }
+  }
+
+  public static void disableReadOnlyMode(Configuration conf, HMaster hMaster,
+    HRegionServer hRegionServer) {
+    if (ConfigurationUtil.isReadOnlyModeEnabledInConf(conf)) {
+      LOG.info("Dynamically disabling Read-Only mode by setting {} to false",
+        HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY);
+      conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
+      notifyReadOnlyObservers(conf, hMaster, hRegionServer);
+    }
+  }
+
+  public static void notifyReadOnlyObservers(Configuration conf, HMaster 
hMaster,
+    HRegionServer hRegionServer) {
+    LOG.info("Notifying observers about configuration changes");
+    hMaster.getConfigurationManager().notifyAllObservers(conf);
+    hRegionServer.getConfigurationManager().notifyAllObservers(conf);
+  }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyController.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyController.java
index a94406e4726..7dcf482a7b7 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyController.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyController.java
@@ -40,20 +40,16 @@ import org.apache.hadoop.hbase.regionserver.HRegionServer;
 import org.apache.hadoop.hbase.testclassification.LargeTests;
 import org.apache.hadoop.hbase.testclassification.SecurityTests;
 import org.apache.hadoop.hbase.util.Bytes;
-import org.apache.hadoop.hbase.util.ConfigurationUtil;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Tag(SecurityTests.TAG)
 @Tag(LargeTests.TAG)
 @SuppressWarnings("deprecation")
 public class TestReadOnlyController {
 
-  private static final Logger LOG = 
LoggerFactory.getLogger(TestReadOnlyController.class);
   private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
   private static final TableName TEST_TABLE = 
TableName.valueOf("read_only_test_table");
   private static final byte[] TEST_FAMILY = 
Bytes.toBytes("read_only_table_col_fam");
@@ -107,29 +103,11 @@ public class TestReadOnlyController {
   }
 
   private static void enableReadOnlyMode() {
-    // Dynamically enable Read-Only mode if it is not active
-    if (!ConfigurationUtil.isReadOnlyModeEnabledInConf(conf)) {
-      LOG.info("Dynamically enabling Read-Only mode by setting {} to true",
-        HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
-      conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
-      notifyObservers();
-    }
+    SecureTestUtil.enableReadOnlyMode(conf, hMaster, hRegionServer);
   }
 
   private static void disableReadOnlyMode() {
-    // Dynamically disable Read-Only mode if it is active
-    if (ConfigurationUtil.isReadOnlyModeEnabledInConf(conf)) {
-      LOG.info("Dynamically disabling Read-Only mode by setting {} to false",
-        HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
-      conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
-      notifyObservers();
-    }
-  }
-
-  private static void notifyObservers() {
-    LOG.info("Notifying observers about configuration changes");
-    hMaster.getConfigurationManager().notifyAllObservers(conf);
-    hRegionServer.getConfigurationManager().notifyAllObservers(conf);
+    SecureTestUtil.disableReadOnlyMode(conf, hMaster, hRegionServer);
   }
 
   // The test case for successfully creating a table with Read-Only mode 
disabled happens when
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerFlush.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerFlush.java
new file mode 100644
index 00000000000..f7ce363c231
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerFlush.java
@@ -0,0 +1,134 @@
+/*
+ * 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.security.access;
+
+import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.master.HMaster;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+@Tag(SecurityTests.TAG)
+@Tag(LargeTests.TAG)
+@SuppressWarnings("deprecation")
+public class TestReadOnlyControllerFlush {
+
+  private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
+  private static final TableName TEST_TABLE = 
TableName.valueOf("read_only_flush_test_table");
+  private static final byte[] TEST_FAMILY = 
Bytes.toBytes("read_only_flush_col_fam");
+  private static HRegionServer hRegionServer;
+  private static HMaster hMaster;
+  private static Configuration conf;
+  private static Connection connection;
+  private static SingleProcessHBaseCluster cluster;
+
+  private static Table testTable;
+
+  @BeforeEach
+  public void beforeClass() throws Exception {
+    conf = TEST_UTIL.getConfiguration();
+
+    // Shorten the run time of failed unit tests by limiting retries and the 
session timeout
+    // threshold
+    conf.setInt(HBASE_CLIENT_RETRIES_NUMBER, 1);
+    conf.setInt(HConstants.ZK_SESSION_TIMEOUT, 1000);
+
+    // Set up test class with Read-Only mode disabled so a table can be created
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
+
+    try {
+      // Start the test cluster
+      cluster = TEST_UTIL.startMiniCluster(1);
+
+      hMaster = cluster.getMaster();
+      hRegionServer = 
cluster.getRegionServerThreads().get(0).getRegionServer();
+      connection = ConnectionFactory.createConnection(conf);
+
+      // Create a test table and insert a row so the memstore has data to flush
+      testTable = TEST_UTIL.createTable(TEST_TABLE, TEST_FAMILY);
+      Put put = new Put(Bytes.toBytes("row1"));
+      put.addColumn(TEST_FAMILY, null, Bytes.toBytes("value1"));
+      testTable.put(put);
+    } catch (Exception e) {
+      disableReadOnlyMode();
+      TEST_UTIL.deleteTable(TEST_TABLE);
+      if (connection != null) {
+        connection.close();
+      }
+      TEST_UTIL.shutdownMiniCluster();
+      throw new RuntimeException(e);
+    }
+  }
+
+  @AfterEach
+  public void afterClass() throws Exception {
+    if (connection != null) {
+      connection.close();
+    }
+    TEST_UTIL.shutdownMiniCluster();
+  }
+
+  private static void enableReadOnlyMode() {
+    SecureTestUtil.enableReadOnlyMode(conf, hMaster, hRegionServer);
+  }
+
+  private static void disableReadOnlyMode() {
+    SecureTestUtil.disableReadOnlyMode(conf, hMaster, hRegionServer);
+  }
+
+  @Test
+  public void testFlushTableWithReadOnlyDisabled() throws IOException {
+    disableReadOnlyMode();
+    try (Admin admin = TEST_UTIL.getAdmin()) {
+      admin.flush(TEST_TABLE);
+    }
+  }
+
+  @Test
+  @Timeout(value = 60, unit = TimeUnit.SECONDS)
+  public void testCannotFlushTableWithReadOnlyEnabled() throws IOException {
+    enableReadOnlyMode();
+    try (Admin admin = TEST_UTIL.getAdmin()) {
+      IOException exception = assertThrows(IOException.class, () -> {
+        admin.flush(TEST_TABLE);
+      });
+      assertTrue(exception.getMessage().contains("Operation not allowed in 
Read-Only Mode"));
+    }
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerMasterObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerMasterObserver.java
index f0d00c809ab..a93c7ade552 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerMasterObserver.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerMasterObserver.java
@@ -318,6 +318,13 @@ public class TestReadOnlyControllerMasterObserver {
     });
   }
 
+  @Test
+  public void testPreTableFlushReadOnlyException() {
+    assertThrows(WriteAttemptedOnReadOnlyClusterException.class, () -> {
+      MasterReadOnlyController.preTableFlush(ctx, tableName);
+    });
+  }
+
   @Test
   public void testPreSetUserQuotaReadOnlyException() {
     assertThrows(WriteAttemptedOnReadOnlyClusterException.class, () -> {

Reply via email to