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, () -> {