This is an automated email from the ASF dual-hosted git repository. andor pushed a commit to branch HBASE-29081 in repository https://gitbox.apache.org/repos/asf/hbase.git
commit ae8fd619093f4a50c6d81a3b10f1de009a746893 Author: Kevin Geiszler <[email protected]> AuthorDate: Thu May 29 05:40:52 2025 -0700 HBASE-29236: Add Support for Dynamic Configuration at the Coprocessor Level (#6931) Co-authored-by: Andor Molnar <[email protected]> Co-authored-by: Anuj Sharma <[email protected]> --- .../hadoop/hbase/coprocessor/CoprocessorHost.java | 35 +++++ .../org/apache/hadoop/hbase/master/HMaster.java | 8 +- .../apache/hadoop/hbase/regionserver/HRegion.java | 12 ++ .../hadoop/hbase/regionserver/HRegionServer.java | 6 + .../hbase/security/access/ReadOnlyController.java | 44 +++--- .../access/TestCanStartHBaseInReadOnlyMode.java | 74 +++++++++ .../security/access/TestReadOnlyController.java | 170 ++++++++++++++++++--- src/main/asciidoc/_chapters/configuration.adoc | 1 + 8 files changed, 309 insertions(+), 41 deletions(-) diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/CoprocessorHost.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/CoprocessorHost.java index 137fe3b061d..191e9573e90 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/CoprocessorHost.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/coprocessor/CoprocessorHost.java @@ -37,6 +37,8 @@ import org.apache.hadoop.hbase.Coprocessor; import org.apache.hadoop.hbase.CoprocessorEnvironment; import org.apache.hadoop.hbase.DoNotRetryIOException; import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.conf.ConfigurationManager; +import org.apache.hadoop.hbase.conf.ConfigurationObserver; import org.apache.hadoop.hbase.ipc.RpcServer; import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.util.CoprocessorClassLoader; @@ -116,6 +118,39 @@ public abstract class CoprocessorHost<C extends Coprocessor, E extends Coprocess return returnValue; } + /** + * Used to help make the relevant loaded coprocessors dynamically configurable by registering them + * to the {@link ConfigurationManager}. Coprocessors are considered "relevant" if they implement + * the {@link ConfigurationObserver} interface. + * @param configurationManager the ConfigurationManager the coprocessors get registered to + */ + public void registerConfigurationObservers(ConfigurationManager configurationManager) { + Coprocessor foundCp; + Set<String> coprocessors = this.getCoprocessors(); + for (String cp : coprocessors) { + foundCp = this.findCoprocessor(cp); + if (foundCp instanceof ConfigurationObserver) { + configurationManager.registerObserver((ConfigurationObserver) foundCp); + } + } + } + + /** + * Deregisters relevant coprocessors from the {@link ConfigurationManager}. Coprocessors are + * considered "relevant" if they implement the {@link ConfigurationObserver} interface. + * @param configurationManager the ConfigurationManager the coprocessors get deregistered from + */ + public void deregisterConfigurationObservers(ConfigurationManager configurationManager) { + Coprocessor foundCp; + Set<String> coprocessors = this.getCoprocessors(); + for (String cp : coprocessors) { + foundCp = this.findCoprocessor(cp); + if (foundCp instanceof ConfigurationObserver) { + configurationManager.deregisterObserver((ConfigurationObserver) foundCp); + } + } + } + /** * Load system coprocessors once only. Read the class names from configuration. Called by * constructor. diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java index 19f58ebe6ad..1282af5806d 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java @@ -610,6 +610,12 @@ public class HMaster extends HBaseServerBase<MasterRpcServices> implements Maste private void registerConfigurationObservers() { configurationManager.registerObserver(this.rpcServices); configurationManager.registerObserver(this); + if (cpHost != null) { + cpHost.registerConfigurationObservers(configurationManager); + } else { + LOG.warn("Could not register HMaster coprocessors to the ConfigurationManager because " + + "MasterCoprocessorHost is null"); + } } // Main run loop. Calls through to the regionserver run loop AFTER becoming active Master; will @@ -618,7 +624,6 @@ public class HMaster extends HBaseServerBase<MasterRpcServices> implements Maste public void run() { try { installShutdownHook(); - registerConfigurationObservers(); Threads.setDaemonThreadRunning(new Thread(TraceUtil.tracedRunnable(() -> { try { int infoPort = putUpJettyServer(); @@ -4513,6 +4518,7 @@ public class HMaster extends HBaseServerBase<MasterRpcServices> implements Maste private void initializeCoprocessorHost(Configuration conf) { // initialize master side coprocessors before we start handling requests this.cpHost = new MasterCoprocessorHost(this, conf); + registerConfigurationObservers(); } @Override diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java index 1801b4d971e..69b7675bb8e 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java @@ -8833,6 +8833,12 @@ public class HRegion implements HeapSize, PropagatingConfigurationObserver, Regi public void registerChildren(ConfigurationManager manager) { configurationManager = manager; stores.values().forEach(manager::registerObserver); + if (coprocessorHost != null) { + coprocessorHost.registerConfigurationObservers(manager); + } else { + LOG.warn("Could not register HRegion coprocessors to the ConfigurationManager because " + + "RegionCoprocessorHost is null"); + } } /** @@ -8841,6 +8847,12 @@ public class HRegion implements HeapSize, PropagatingConfigurationObserver, Regi @Override public void deregisterChildren(ConfigurationManager manager) { stores.values().forEach(configurationManager::deregisterObserver); + if (coprocessorHost != null) { + coprocessorHost.deregisterConfigurationObservers(manager); + } else { + LOG.warn("Could not deregister HRegion coprocessors from the ConfigurationManager because " + + "RegionCoprocessorHost is null"); + } } @Override diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index 2bddf7f9d27..bfba25fc175 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -2142,6 +2142,12 @@ public class HRegionServer extends HBaseServerBase<RSRpcServices> configurationManager.registerObserver(this.rpcServices); configurationManager.registerObserver(this.prefetchExecutorNotifier); configurationManager.registerObserver(this); + if (rsHost != null) { + rsHost.registerConfigurationObservers(configurationManager); + } else { + LOG.warn("Could not register HRegionServer coprocessors to the ConfigurationManager because " + + "RegionServerCoprocessorHost is null"); + } } /* diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java index 90d154ebec5..13f458299b9 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ReadOnlyController.java @@ -35,6 +35,7 @@ import org.apache.hadoop.hbase.client.RegionInfo; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.SnapshotDescription; import org.apache.hadoop.hbase.client.TableDescriptor; +import org.apache.hadoop.hbase.conf.ConfigurationObserver; import org.apache.hadoop.hbase.coprocessor.BulkLoadObserver; import org.apache.hadoop.hbase.coprocessor.CoreCoprocessor; import org.apache.hadoop.hbase.coprocessor.EndpointObserver; @@ -65,26 +66,24 @@ import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; @CoreCoprocessor @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG) -public class ReadOnlyController - implements MasterCoprocessor, RegionCoprocessor, MasterObserver, RegionObserver, - RegionServerCoprocessor, RegionServerObserver, EndpointObserver, BulkLoadObserver { +public class ReadOnlyController implements MasterCoprocessor, RegionCoprocessor, MasterObserver, + RegionObserver, RegionServerCoprocessor, RegionServerObserver, EndpointObserver, BulkLoadObserver, + ConfigurationObserver { private static final Logger LOG = LoggerFactory.getLogger(ReadOnlyController.class); - private Configuration conf; + private volatile boolean globalReadOnlyEnabled; private void internalReadOnlyGuard() throws IOException { - if ( - conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, - HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT) - ) { - // throw new FailedSanityCheckException("Operation not allowed in Read-Only Mode"); + if (this.globalReadOnlyEnabled) { throw new IOException("Operation not allowed in Read-Only Mode"); } } @Override public void start(CoprocessorEnvironment env) throws IOException { - conf = env.getConfiguration(); + this.globalReadOnlyEnabled = + env.getConfiguration().getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, + HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT); } @Override @@ -100,7 +99,8 @@ public class ReadOnlyController @Override public void prePut(ObserverContext<? extends RegionCoprocessorEnvironment> c, Put put, WALEdit edit) throws IOException { - if (edit.isMetaEdit() || edit.isEmpty()) { + TableName tableName = c.getEnvironment().getRegionInfo().getTable(); + if (tableName.isSystemTable()) { return; } internalReadOnlyGuard(); @@ -115,13 +115,11 @@ public class ReadOnlyController @Override public void preBatchMutate(ObserverContext<? extends RegionCoprocessorEnvironment> c, MiniBatchOperationInProgress<Mutation> miniBatchOp) throws IOException { - for (int i = 0; i < miniBatchOp.size(); i++) { - WALEdit edit = miniBatchOp.getWalEdit(i); - if (edit == null || edit.isMetaEdit() || edit.isEmpty()) { - continue; - } - internalReadOnlyGuard(); + TableName tableName = c.getEnvironment().getRegionInfo().getTable(); + if (tableName.isSystemTable()) { + return; } + internalReadOnlyGuard(); } @Override @@ -390,4 +388,16 @@ public class ReadOnlyController internalReadOnlyGuard(); BulkLoadObserver.super.preCleanupBulkLoad(ctx); } + + /* ---- ConfigurationObserver Overrides ---- */ + @Override + public void onConfigurationChange(Configuration conf) { + boolean maybeUpdatedConfValue = conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, + HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT); + if (this.globalReadOnlyEnabled != maybeUpdatedConfValue) { + this.globalReadOnlyEnabled = maybeUpdatedConfValue; + LOG.info("Config {} has been dynamically changed to {}", + HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, this.globalReadOnlyEnabled); + } + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestCanStartHBaseInReadOnlyMode.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestCanStartHBaseInReadOnlyMode.java new file mode 100644 index 00000000000..ed182edbe91 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestCanStartHBaseInReadOnlyMode.java @@ -0,0 +1,74 @@ +/* + * 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 org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.*; +import org.apache.hadoop.hbase.client.*; +import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; +import org.apache.hadoop.hbase.testclassification.LargeTests; +import org.apache.hadoop.hbase.testclassification.SecurityTests; +import org.junit.*; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestName; + +@Category({ SecurityTests.class, LargeTests.class }) +@SuppressWarnings("deprecation") +public class TestCanStartHBaseInReadOnlyMode { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestCanStartHBaseInReadOnlyMode.class); + + private static final String READ_ONLY_CONTROLLER_NAME = ReadOnlyController.class.getName(); + private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); + private static Configuration conf; + + @Rule + public TestName name = new TestName(); + + @BeforeClass + public static 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, 0); + conf.setInt("hbase.master.init.timeout.localHBaseCluster", 10000); + + // Enable Read-Only mode to prove the cluster can be started in this state + conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true); + + // Add ReadOnlyController coprocessors to the master, region server, and regions + conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, READ_ONLY_CONTROLLER_NAME); + conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, READ_ONLY_CONTROLLER_NAME); + conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, READ_ONLY_CONTROLLER_NAME); + } + + @AfterClass + public static void afterClass() throws Exception { + TEST_UTIL.shutdownMiniCluster(); + } + + @Test + public void testCanStartHBaseInReadOnlyMode() throws Exception { + TEST_UTIL.startMiniCluster(1); + } +} 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 1b286214e6d..ddf513fcfe7 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 @@ -17,17 +17,26 @@ */ package org.apache.hadoop.hbase.security.access; +import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER; + import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HBaseClassTestRule; 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.Connection; import org.apache.hadoop.hbase.client.ConnectionFactory; +import org.apache.hadoop.hbase.client.Delete; +import org.apache.hadoop.hbase.client.Put; +import org.apache.hadoop.hbase.client.Row; import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; -import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessorEnvironment; +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; @@ -50,16 +59,18 @@ public class TestReadOnlyController { public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestReadOnlyController.class); - private static final Logger LOG = LoggerFactory.getLogger(TestAccessController.class); + private static final Logger LOG = LoggerFactory.getLogger(TestReadOnlyController.class); + private static final String READ_ONLY_CONTROLLER_NAME = ReadOnlyController.class.getName(); private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); - private static TableName TEST_TABLE = TableName.valueOf("readonlytesttable"); - private static byte[] TEST_FAMILY = Bytes.toBytes("readonlytablecolfam"); + 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"); + private static HRegionServer hRegionServer; + private static HMaster hMaster; private static Configuration conf; private static Connection connection; + private static SingleProcessHBaseCluster cluster; - private static RegionServerCoprocessorEnvironment RSCP_ENV; - - private static Table TestTable; + private static Table testTable; @Rule public TestName name = new TestName(); @@ -69,20 +80,39 @@ public class TestReadOnlyController { @BeforeClass public static void beforeClass() throws Exception { conf = TEST_UTIL.getConfiguration(); - // Only try once so that if there is failure in connection then test should fail faster - conf.setInt("hbase.ipc.client.connect.max.retries", 1); - // Shorter session timeout is added so that in case failures test should not take more time + + // 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); - // Enable ReadOnly mode for the cluster - conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true); - // Add the ReadOnlyController coprocessor for region server to interrupt any write operation - conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, ReadOnlyController.class.getName()); - // Add the ReadOnlyController coprocessor to for master to interrupt any write operation - conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, ReadOnlyController.class.getName()); - // Start the test cluster - TEST_UTIL.startMiniCluster(2); - // Get connection to the HBase - connection = ConnectionFactory.createConnection(conf); + + // Set up test class with Read-Only mode disabled so a table can be created + conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false); + + // Add ReadOnlyController coprocessors to the master, region server, and regions + conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, READ_ONLY_CONTROLLER_NAME); + conf.set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, READ_ONLY_CONTROLLER_NAME); + conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, READ_ONLY_CONTROLLER_NAME); + + 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 + testTable = TEST_UTIL.createTable(TEST_TABLE, TEST_FAMILY); + } catch (Exception e) { + // Delete the created table, and clean up the connection and cluster before throwing an + // exception + disableReadOnlyMode(); + TEST_UTIL.deleteTable(TEST_TABLE); + connection.close(); + TEST_UTIL.shutdownMiniCluster(); + throw new RuntimeException(e); + } } @AfterClass @@ -93,8 +123,102 @@ public class TestReadOnlyController { TEST_UTIL.shutdownMiniCluster(); } - @Test(expected = IOException.class) - public void testCreateTable() throws IOException { - TEST_UTIL.createTable(TEST_TABLE, TEST_FAMILY); + private static void enableReadOnlyMode() { + // Dynamically enable Read-Only mode if it is not active + if (!isReadOnlyModeEnabled()) { + 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(); + } + } + + private static void disableReadOnlyMode() { + // Dynamically disable Read-Only mode if it is active + if (isReadOnlyModeEnabled()) { + 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 boolean isReadOnlyModeEnabled() { + return conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, + HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT); + } + + private static void notifyObservers() { + LOG.info("Notifying observers about configuration changes"); + hMaster.getConfigurationManager().notifyAllObservers(conf); + hRegionServer.getConfigurationManager().notifyAllObservers(conf); + } + + // The test case for successfully creating a table with Read-Only mode disabled happens when + // setting up the test class, so we only need a test function for a failed table creation. + @Test + public void testCannotCreateTableWithReadOnlyEnabled() throws IOException { + // Expect an IOException to result from the createTable attempt since Read-Only mode is enabled + enableReadOnlyMode(); + TableName newTable = TableName.valueOf("bad_read_only_test_table"); + exception.expect(IOException.class); + exception.expectMessage("Operation not allowed in Read-Only Mode"); + + // This should throw the IOException + TEST_UTIL.createTable(newTable, TEST_FAMILY); + } + + @Test + public void testPutWithReadOnlyDisabled() throws IOException { + // Successfully put a row in the table since Read-Only mode is disabled + disableReadOnlyMode(); + final byte[] row2 = Bytes.toBytes("row2"); + final byte[] value = Bytes.toBytes("efgh"); + Put put = new Put(row2); + put.addColumn(TEST_FAMILY, null, value); + testTable.put(put); + } + + @Test + public void testCannotPutWithReadOnlyEnabled() throws IOException { + // Prepare a Put command with Read-Only mode enabled + enableReadOnlyMode(); + final byte[] row1 = Bytes.toBytes("row1"); + final byte[] value = Bytes.toBytes("abcd"); + Put put = new Put(row1); + put.addColumn(TEST_FAMILY, null, value); + + // Expect an IOException to result from the Put attempt + exception.expect(IOException.class); + exception.expectMessage("Operation not allowed in Read-Only Mode"); + + // This should throw the IOException + testTable.put(put); + } + + @Test + public void testBatchPutWithReadOnlyDisabled() throws IOException, InterruptedException { + // Successfully create and run a batch Put operation with Read-Only mode disabled + disableReadOnlyMode(); + List<Row> actions = new ArrayList<>(); + actions.add(new Put(Bytes.toBytes("row10")).addColumn(TEST_FAMILY, null, Bytes.toBytes("10"))); + actions.add(new Delete(Bytes.toBytes("row10"))); + testTable.batch(actions, null); + } + + @Test + public void testCannotBatchPutWithReadOnlyEnabled() throws IOException, InterruptedException { + // Create a batch Put operation that is expected to fail with Read-Only mode enabled + enableReadOnlyMode(); + List<Row> actions = new ArrayList<>(); + actions.add(new Put(Bytes.toBytes("row11")).addColumn(TEST_FAMILY, null, Bytes.toBytes("11"))); + actions.add(new Delete(Bytes.toBytes("row11"))); + + // Expect an IOException to result from the batch Put attempt + exception.expect(IOException.class); + exception.expectMessage("Operation not allowed in Read-Only Mode"); + + // This should throw the IOException + testTable.batch(actions, null); } } diff --git a/src/main/asciidoc/_chapters/configuration.adoc b/src/main/asciidoc/_chapters/configuration.adoc index a990863900f..8764b78aa63 100644 --- a/src/main/asciidoc/_chapters/configuration.adoc +++ b/src/main/asciidoc/_chapters/configuration.adoc @@ -1477,6 +1477,7 @@ Here are those configurations: | hbase.coprocessor.region.classes | hbase.coprocessor.regionserver.classes | hbase.coprocessor.user.region.classes +| hbase.global.readonly.enabled | hbase.regionserver.thread.compaction.large | hbase.regionserver.thread.compaction.small | hbase.regionserver.thread.split
