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

Reply via email to