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


The following commit(s) were added to refs/heads/HBASE-29081 by this push:
     new a54b00684f1 HBASE-29756: Programmatically register related 
co-processor during initialization (#7743)
a54b00684f1 is described below

commit a54b00684f17d29ee7e5d288639ed12d6f97a00a
Author: Anuj Sharma <[email protected]>
AuthorDate: Fri Feb 27 22:50:31 2026 +0530

    HBASE-29756: Programmatically register related co-processor during 
initialization (#7743)
    
    * HBASE-29756: Programmatically register related co-processor during 
initialization
    
    * Apply Spotless
    
    * Remove the cached globalReadOnlyMode variable and make 
manageclusterIDFile static
    
    * Address review comments
    
    * Address review comments
    
    * Make coprocessor addition and removal generic
    
    * Make manageClusterIdFile Idempotent
    
    * Address review comments
    
    * Avoid intelliJ warning about fixed size array creation
---
 .../org/apache/hadoop/hbase/master/HMaster.java    |  16 +
 .../apache/hadoop/hbase/regionserver/HRegion.java  |  13 +
 .../hadoop/hbase/regionserver/HRegionServer.java   |  11 +
 .../access/AbstractReadOnlyController.java         |  59 +---
 .../hadoop/hbase/util/ConfigurationUtil.java       |   6 +
 .../hbase/util/CoprocessorConfigurationUtil.java   |  86 +++++
 .../apache/hadoop/hbase/TestRefreshHFilesBase.java |  14 -
 .../TestRefreshMetaProcedureIntegration.java       |  15 -
 .../access/TestCanStartHBaseInReadOnlyMode.java    |  10 -
 .../security/access/TestReadOnlyController.java    |  24 +-
 .../TestReadOnlyControllerBulkLoadObserver.java    |  17 -
 .../TestReadOnlyControllerCoprocessorLoading.java  | 258 +++++++++++++++
 .../TestReadOnlyControllerEndpointObserver.java    |  12 -
 .../TestReadOnlyControllerMasterObserver.java      | 299 -----------------
 .../TestReadOnlyControllerRegionObserver.java      | 360 ---------------------
 ...TestReadOnlyControllerRegionServerObserver.java |  23 --
 .../util/TestCoprocessorConfigurationUtil.java     | 200 ++++++++++++
 17 files changed, 613 insertions(+), 810 deletions(-)

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 c88fb535ce2..5c7bf34b1c5 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
@@ -250,10 +250,12 @@ import 
org.apache.hadoop.hbase.security.AccessDeniedException;
 import org.apache.hadoop.hbase.security.SecurityConstants;
 import org.apache.hadoop.hbase.security.Superusers;
 import org.apache.hadoop.hbase.security.UserProvider;
+import org.apache.hadoop.hbase.security.access.AbstractReadOnlyController;
 import org.apache.hadoop.hbase.trace.TraceUtil;
 import org.apache.hadoop.hbase.util.Addressing;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.CommonFSUtils;
+import org.apache.hadoop.hbase.util.ConfigurationUtil;
 import org.apache.hadoop.hbase.util.CoprocessorConfigurationUtil;
 import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
 import org.apache.hadoop.hbase.util.FSTableDescriptors;
@@ -1086,6 +1088,11 @@ public class HMaster extends 
HBaseServerBase<MasterRpcServices> implements Maste
     if (!maintenanceMode) {
       startupTaskGroup.addTask("Initializing master coprocessors");
       setQuotasObserver(conf);
+      CoprocessorConfigurationUtil.syncReadOnlyConfigurations(
+        ConfigurationUtil.isReadOnlyModeEnabled(conf), conf,
+        CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY);
+      AbstractReadOnlyController.manageActiveClusterIdFile(
+        ConfigurationUtil.isReadOnlyModeEnabled(conf), 
this.getMasterFileSystem());
       initializeCoprocessorHost(conf);
     } else {
       // start an in process region server for carrying system regions
@@ -4422,6 +4429,11 @@ public class HMaster extends 
HBaseServerBase<MasterRpcServices> implements Maste
     }
     // append the quotas observer back to the master coprocessor key
     setQuotasObserver(newConf);
+
+    boolean readOnlyMode = ConfigurationUtil.isReadOnlyModeEnabled(newConf);
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(readOnlyMode, 
newConf,
+      CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY);
+
     // update region server coprocessor if the configuration has changed.
     if (
       
CoprocessorConfigurationUtil.checkConfigurationChange(getConfiguration(), 
newConf,
@@ -4429,6 +4441,10 @@ public class HMaster extends 
HBaseServerBase<MasterRpcServices> implements Maste
     ) {
       LOG.info("Update the master coprocessor(s) because the configuration has 
changed");
       initializeCoprocessorHost(newConf);
+      CoprocessorConfigurationUtil.syncReadOnlyConfigurations(readOnlyMode, 
this.conf,
+        CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY);
+      AbstractReadOnlyController.manageActiveClusterIdFile(
+        ConfigurationUtil.isReadOnlyModeEnabled(newConf), 
this.getMasterFileSystem());
     }
   }
 
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 7c55241e5fe..357b035c537 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
@@ -174,6 +174,7 @@ import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.CancelableProgressable;
 import org.apache.hadoop.hbase.util.ClassSize;
 import org.apache.hadoop.hbase.util.CommonFSUtils;
+import org.apache.hadoop.hbase.util.ConfigurationUtil;
 import org.apache.hadoop.hbase.util.CoprocessorConfigurationUtil;
 import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
 import org.apache.hadoop.hbase.util.FSUtils;
@@ -885,6 +886,11 @@ public class HRegion implements HeapSize, 
PropagatingConfigurationObserver, Regi
       : this.htableDescriptor.getDurability();
 
     decorateRegionConfiguration(conf);
+
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(
+      ConfigurationUtil.isReadOnlyModeEnabled(conf), this.conf,
+      CoprocessorHost.REGION_COPROCESSOR_CONF_KEY);
+
     if (rsServices != null) {
       this.rsAccounting = this.rsServices.getRegionServerAccounting();
       // don't initialize coprocessors if not running within a regionserver
@@ -8814,6 +8820,11 @@ public class HRegion implements HeapSize, 
PropagatingConfigurationObserver, Regi
   @Override
   public void onConfigurationChange(Configuration conf) {
     this.storeHotnessProtector.update(conf);
+
+    boolean readOnlyMode = ConfigurationUtil.isReadOnlyModeEnabled(conf);
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(readOnlyMode, conf,
+      CoprocessorHost.REGION_COPROCESSOR_CONF_KEY);
+
     // update coprocessorHost if the configuration has changed.
     if (
       
CoprocessorConfigurationUtil.checkConfigurationChange(getReadOnlyConfiguration(),
 conf,
@@ -8823,6 +8834,8 @@ public class HRegion implements HeapSize, 
PropagatingConfigurationObserver, Regi
       LOG.info("Update the system coprocessors because the configuration has 
changed");
       decorateRegionConfiguration(conf);
       this.coprocessorHost = new RegionCoprocessorHost(this, rsServices, conf);
+      CoprocessorConfigurationUtil.syncReadOnlyConfigurations(readOnlyMode, 
this.conf,
+        CoprocessorHost.REGION_COPROCESSOR_CONF_KEY);
     }
   }
 
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 86a9880e6b1..796a019e0f8 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
@@ -161,6 +161,7 @@ import org.apache.hadoop.hbase.security.UserProvider;
 import org.apache.hadoop.hbase.trace.TraceUtil;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.CompressionTest;
+import org.apache.hadoop.hbase.util.ConfigurationUtil;
 import org.apache.hadoop.hbase.util.CoprocessorConfigurationUtil;
 import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
 import org.apache.hadoop.hbase.util.FSUtils;
@@ -825,6 +826,10 @@ public class HRegionServer extends 
HBaseServerBase<RSRpcServices>
     try {
       if (!isStopped() && !isAborted()) {
         installShutdownHook();
+
+        CoprocessorConfigurationUtil.syncReadOnlyConfigurations(
+          ConfigurationUtil.isReadOnlyModeEnabled(conf), conf,
+          CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY);
         // Initialize the RegionServerCoprocessorHost now that our ephemeral
         // node was created, in case any coprocessors want to use ZooKeeper
         this.rsHost = new RegionServerCoprocessorHost(this, this.conf);
@@ -3480,6 +3485,10 @@ public class HRegionServer extends 
HBaseServerBase<RSRpcServices>
       LOG.warn("Failed to initialize SuperUsers on reloading of the 
configuration");
     }
 
+    boolean readOnlyMode = ConfigurationUtil.isReadOnlyModeEnabled(newConf);
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(readOnlyMode, 
newConf,
+      CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY);
+
     // update region server coprocessor if the configuration has changed.
     if (
       
CoprocessorConfigurationUtil.checkConfigurationChange(getConfiguration(), 
newConf,
@@ -3487,6 +3496,8 @@ public class HRegionServer extends 
HBaseServerBase<RSRpcServices>
     ) {
       LOG.info("Update region server coprocessors because the configuration 
has changed");
       this.rsHost = new RegionServerCoprocessorHost(this, newConf);
+      CoprocessorConfigurationUtil.syncReadOnlyConfigurations(readOnlyMode, 
this.conf,
+        CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY);
     }
   }
 
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AbstractReadOnlyController.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AbstractReadOnlyController.java
index 402009c025a..d5039f84348 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AbstractReadOnlyController.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AbstractReadOnlyController.java
@@ -18,7 +18,6 @@
 package org.apache.hadoop.hbase.security.access;
 
 import java.io.IOException;
-import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.Coprocessor;
@@ -26,8 +25,6 @@ import org.apache.hadoop.hbase.CoprocessorEnvironment;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HBaseInterfaceAudience;
 import org.apache.hadoop.hbase.HConstants;
-import org.apache.hadoop.hbase.conf.ConfigurationObserver;
-import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment;
 import org.apache.hadoop.hbase.master.MasterFileSystem;
 import org.apache.hadoop.hbase.master.MasterServices;
 import org.apache.hadoop.hbase.util.FSUtils;
@@ -35,40 +32,24 @@ import org.apache.yetus.audience.InterfaceAudience;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-
 @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
-public abstract class AbstractReadOnlyController implements 
ConfigurationObserver, Coprocessor {
-  protected volatile boolean globalReadOnlyEnabled;
+public abstract class AbstractReadOnlyController implements Coprocessor {
   private MasterServices masterServices;
   private static final Logger LOG = 
LoggerFactory.getLogger(AbstractReadOnlyController.class);
 
   protected void internalReadOnlyGuard() throws DoNotRetryIOException {
-    if (this.globalReadOnlyEnabled) {
-      throw new DoNotRetryIOException("Operation not allowed in Read-Only 
Mode");
-    }
+    throw new DoNotRetryIOException("Operation not allowed in Read-Only Mode");
   }
 
   @Override
   public void start(CoprocessorEnvironment env) throws IOException {
-    if (env instanceof MasterCoprocessorEnvironment) {
-      this.masterServices = ((MasterCoprocessorEnvironment) 
env).getMasterServices();
-      LOG.info("ReadOnlyController obtained MasterServices reference from 
start().");
-    } else {
-      LOG.debug("ReadOnlyController loaded in a non-Master environment. "
-        + "File system operations for read-only state will not work.");
-    }
-
-    this.globalReadOnlyEnabled =
-      
env.getConfiguration().getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
-        HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
   }
 
   @Override
   public void stop(CoprocessorEnvironment env) {
   }
 
-  private void manageActiveClusterIdFile(boolean readOnlyEnabled) {
-    MasterFileSystem mfs = this.masterServices.getMasterFileSystem();
+  public static void manageActiveClusterIdFile(boolean readOnlyEnabled, 
MasterFileSystem mfs) {
     FileSystem fs = mfs.getFileSystem();
     Path rootDir = mfs.getRootDir();
     Path activeClusterFile = new Path(rootDir, 
HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME);
@@ -79,8 +60,13 @@ public abstract class AbstractReadOnlyController implements 
ConfigurationObserve
         LOG.debug("Global read-only mode is being ENABLED. Deleting active 
cluster file: {}",
           activeClusterFile);
         try {
-          fs.delete(activeClusterFile, false);
-          LOG.info("Successfully deleted active cluster file: {}", 
activeClusterFile);
+          if (fs.exists(activeClusterFile)) {
+            fs.delete(activeClusterFile, false);
+            LOG.info("Successfully deleted active cluster file: {}", 
activeClusterFile);
+          } else {
+            LOG.debug("Active cluster file does not exist at: {}. No need to 
delete.",
+              activeClusterFile);
+          }
         } catch (IOException e) {
           LOG.error(
             "Failed to delete active cluster file: {}. "
@@ -90,7 +76,12 @@ public abstract class AbstractReadOnlyController implements 
ConfigurationObserve
       } else {
         // DISABLING READ-ONLY (true -> false), create the active cluster file 
id file
         int wait = 
mfs.getConfiguration().getInt(HConstants.THREAD_WAKE_FREQUENCY, 10 * 1000);
-        FSUtils.setActiveClusterSuffix(fs, rootDir, 
mfs.getSuffixFileDataToWrite(), wait);
+        if (!fs.exists(activeClusterFile)) {
+          FSUtils.setActiveClusterSuffix(fs, rootDir, 
mfs.getSuffixFileDataToWrite(), wait);
+        } else {
+          LOG.debug("Active cluster file already exists at: {}. No need to 
create it again.",
+            activeClusterFile);
+        }
       }
     } catch (IOException e) {
       // We still update the flag, but log that the operation failed.
@@ -98,22 +89,4 @@ public abstract class AbstractReadOnlyController implements 
ConfigurationObserve
         + "Flag will be updated, but file system state may be inconsistent.", 
e);
     }
   }
-
-  @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) {
-      if (this.masterServices != null) {
-        manageActiveClusterIdFile(maybeUpdatedConfValue);
-      } else {
-        LOG.debug("Global R/O flag changed, but not running on master");
-      }
-
-      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/main/java/org/apache/hadoop/hbase/util/ConfigurationUtil.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ConfigurationUtil.java
index 555d1fa5a8a..aed30e46af7 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ConfigurationUtil.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/ConfigurationUtil.java
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.util.StringUtils;
 import org.apache.yetus.audience.InterfaceAudience;
 
@@ -114,4 +115,9 @@ public final class ConfigurationUtil {
     }
     return rtn;
   }
+
+  public static boolean isReadOnlyModeEnabled(Configuration conf) {
+    return conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
+      HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
+  }
 }
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/CoprocessorConfigurationUtil.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/CoprocessorConfigurationUtil.java
index 93c88a89771..d5e564a804c 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/CoprocessorConfigurationUtil.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/CoprocessorConfigurationUtil.java
@@ -17,8 +17,18 @@
  */
 package org.apache.hadoop.hbase.util;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
+import org.apache.hadoop.hbase.security.access.BulkLoadReadOnlyController;
+import org.apache.hadoop.hbase.security.access.EndpointReadOnlyController;
+import org.apache.hadoop.hbase.security.access.MasterReadOnlyController;
+import org.apache.hadoop.hbase.security.access.RegionReadOnlyController;
+import org.apache.hadoop.hbase.security.access.RegionServerReadOnlyController;
 import org.apache.yetus.audience.InterfaceAudience;
 
 import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
@@ -47,4 +57,80 @@ public final class CoprocessorConfigurationUtil {
     }
     return isConfigurationChange;
   }
+
+  private static List<String> getCoprocessorsFromConfig(Configuration conf,
+    String configurationKey) {
+    String[] existing = conf.getStrings(configurationKey);
+    return existing != null ? new ArrayList<>(Arrays.asList(existing)) : new 
ArrayList<>();
+  }
+
+  public static void addCoprocessors(Configuration conf, String 
configurationKey,
+    List<String> coprocessorsToAdd) {
+    List<String> existing = getCoprocessorsFromConfig(conf, configurationKey);
+
+    boolean isModified = false;
+
+    for (String coprocessor : coprocessorsToAdd) {
+      if (!existing.contains(coprocessor)) {
+        existing.add(coprocessor);
+        isModified = true;
+      }
+    }
+
+    if (isModified) {
+      conf.setStrings(configurationKey, existing.toArray(new String[0]));
+    }
+  }
+
+  public static void removeCoprocessors(Configuration conf, String 
configurationKey,
+    List<String> coprocessorsToRemove) {
+    List<String> existing = getCoprocessorsFromConfig(conf, configurationKey);
+
+    if (existing.isEmpty()) {
+      return;
+    }
+
+    boolean isModified = false;
+
+    for (String coprocessor : coprocessorsToRemove) {
+      if (existing.contains(coprocessor)) {
+        existing.remove(coprocessor);
+        isModified = true;
+      }
+    }
+
+    if (isModified) {
+      conf.setStrings(configurationKey, existing.toArray(new String[0]));
+    }
+  }
+
+  private static List<String> getReadOnlyCoprocessors(String configurationKey) 
{
+    return switch (configurationKey) {
+      case CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY -> List
+        .of(MasterReadOnlyController.class.getName());
+
+      case CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY -> List
+        .of(RegionServerReadOnlyController.class.getName());
+
+      case CoprocessorHost.REGION_COPROCESSOR_CONF_KEY -> List.of(
+        RegionReadOnlyController.class.getName(), 
BulkLoadReadOnlyController.class.getName(),
+        EndpointReadOnlyController.class.getName());
+
+      default -> throw new IllegalArgumentException(
+        "Unsupported coprocessor configuration key: " + configurationKey);
+    };
+  }
+
+  public static void syncReadOnlyConfigurations(boolean readOnlyMode, 
Configuration conf,
+    String configurationKey) {
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, 
readOnlyMode);
+
+    List<String> cpList = getReadOnlyCoprocessors(configurationKey);
+    // If readonly is true then add the coprocessor of master
+    if (readOnlyMode) {
+      CoprocessorConfigurationUtil.addCoprocessors(conf, configurationKey, 
cpList);
+    } else {
+      CoprocessorConfigurationUtil.removeCoprocessors(conf, configurationKey, 
cpList);
+    }
+  }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/TestRefreshHFilesBase.java 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/TestRefreshHFilesBase.java
index b18503aa68c..5338b7a6260 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/TestRefreshHFilesBase.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/TestRefreshHFilesBase.java
@@ -25,18 +25,12 @@ import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.client.Admin;
-import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
 import org.apache.hadoop.hbase.master.HMaster;
 import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv;
 import org.apache.hadoop.hbase.master.procedure.RefreshHFilesTableProcedure;
 import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
 import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility;
 import org.apache.hadoop.hbase.regionserver.HRegionServer;
-import org.apache.hadoop.hbase.security.access.BulkLoadReadOnlyController;
-import org.apache.hadoop.hbase.security.access.EndpointReadOnlyController;
-import org.apache.hadoop.hbase.security.access.MasterReadOnlyController;
-import org.apache.hadoop.hbase.security.access.RegionReadOnlyController;
-import org.apache.hadoop.hbase.security.access.RegionServerReadOnlyController;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.JVMClusterUtil;
 import org.slf4j.Logger;
@@ -124,14 +118,6 @@ public class TestRefreshHFilesBase {
 
   private void setupReadOnlyConf(boolean addReadOnlyConf) {
     if (!addReadOnlyConf) return;
-    // Configure the cluster with ReadOnlyControllers
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
-      MasterReadOnlyController.class.getName());
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
-      String.join(",", RegionReadOnlyController.class.getName(),
-        BulkLoadReadOnlyController.class.getName(), 
EndpointReadOnlyController.class.getName()));
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
-      RegionServerReadOnlyController.class.getName());
     // Keep ReadOnly property to false at the beginning so that create table 
succeed.
     conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
   }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestRefreshMetaProcedureIntegration.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestRefreshMetaProcedureIntegration.java
index d0fced61975..208c2b04beb 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestRefreshMetaProcedureIntegration.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/procedure/TestRefreshMetaProcedureIntegration.java
@@ -42,16 +42,10 @@ import org.apache.hadoop.hbase.client.RegionInfoBuilder;
 import org.apache.hadoop.hbase.client.Table;
 import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
 import org.apache.hadoop.hbase.client.TableState;
-import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
 import org.apache.hadoop.hbase.master.HMaster;
 import org.apache.hadoop.hbase.procedure2.ProcedureExecutor;
 import org.apache.hadoop.hbase.regionserver.HRegionFileSystem;
 import org.apache.hadoop.hbase.regionserver.HRegionServer;
-import org.apache.hadoop.hbase.security.access.BulkLoadReadOnlyController;
-import org.apache.hadoop.hbase.security.access.EndpointReadOnlyController;
-import org.apache.hadoop.hbase.security.access.MasterReadOnlyController;
-import org.apache.hadoop.hbase.security.access.RegionReadOnlyController;
-import org.apache.hadoop.hbase.security.access.RegionServerReadOnlyController;
 import org.apache.hadoop.hbase.testclassification.LargeTests;
 import org.apache.hadoop.hbase.testclassification.MasterTests;
 import org.apache.hadoop.hbase.util.Bytes;
@@ -77,15 +71,6 @@ public class TestRefreshMetaProcedureIntegration {
 
   @Before
   public void setup() throws Exception {
-    // Configure the cluster with ReadOnlyControllers
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
-      MasterReadOnlyController.class.getName());
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
-      String.join(",", RegionReadOnlyController.class.getName(),
-        BulkLoadReadOnlyController.class.getName(), 
EndpointReadOnlyController.class.getName()));
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
-      RegionServerReadOnlyController.class.getName());
-
     // Start in active mode
     
TEST_UTIL.getConfiguration().setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
 false);
 
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
index 8607b170808..82bca89f443 100644
--- 
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
@@ -22,7 +22,6 @@ 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.*;
@@ -54,15 +53,6 @@ public class TestCanStartHBaseInReadOnlyMode {
 
     // 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
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
-      MasterReadOnlyController.class.getName());
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
-      String.join(",", RegionReadOnlyController.class.getName(),
-        BulkLoadReadOnlyController.class.getName(), 
EndpointReadOnlyController.class.getName()));
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
-      RegionServerReadOnlyController.class.getName());
   }
 
   @AfterClass
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 c7853b8adac..15ee2fdd45b 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
@@ -38,15 +38,14 @@ 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.master.HMaster;
 import org.apache.hadoop.hbase.master.MasterFileSystem;
 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.AfterClass;
-import org.junit.BeforeClass;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
@@ -65,7 +64,7 @@ public class TestReadOnlyController {
     HBaseClassTestRule.forClass(TestReadOnlyController.class);
 
   private static final Logger LOG = 
LoggerFactory.getLogger(TestReadOnlyController.class);
-  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
+  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");
   private static HRegionServer hRegionServer;
@@ -81,8 +80,8 @@ public class TestReadOnlyController {
   @Rule
   public ExpectedException exception = ExpectedException.none();
 
-  @BeforeClass
-  public static void beforeClass() throws Exception {
+  @Before
+  public void beforeClass() throws Exception {
     conf = TEST_UTIL.getConfiguration();
 
     // Shorten the run time of failed unit tests by limiting retries and the 
session timeout
@@ -93,15 +92,6 @@ public class TestReadOnlyController {
     // 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
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
-      MasterReadOnlyController.class.getName());
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
-      String.join(",", RegionReadOnlyController.class.getName(),
-        BulkLoadReadOnlyController.class.getName(), 
EndpointReadOnlyController.class.getName()));
-    
TEST_UTIL.getConfiguration().set(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
-      RegionServerReadOnlyController.class.getName());
-
     try {
       // Start the test cluster
       cluster = TEST_UTIL.startMiniCluster(1);
@@ -123,8 +113,8 @@ public class TestReadOnlyController {
     }
   }
 
-  @AfterClass
-  public static void afterClass() throws Exception {
+  @After
+  public void afterClass() throws Exception {
     if (connection != null) {
       connection.close();
     }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerBulkLoadObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerBulkLoadObserver.java
index 46984217282..9ac008fdaf8 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerBulkLoadObserver.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerBulkLoadObserver.java
@@ -17,13 +17,11 @@
  */
 package org.apache.hadoop.hbase.security.access;
 
-import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
-import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.hadoop.hbase.coprocessor.ObserverContext;
 import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
 import org.apache.hadoop.hbase.testclassification.SecurityTests;
@@ -43,7 +41,6 @@ public class TestReadOnlyControllerBulkLoadObserver {
     HBaseClassTestRule.forClass(TestReadOnlyControllerBulkLoadObserver.class);
 
   BulkLoadReadOnlyController bulkLoadReadOnlyController;
-  HBaseConfiguration readOnlyConf;
 
   // Region Server Coprocessor mocking variables
   ObserverContext<RegionCoprocessorEnvironment> ctx;
@@ -51,8 +48,6 @@ public class TestReadOnlyControllerBulkLoadObserver {
   @Before
   public void setup() throws Exception {
     bulkLoadReadOnlyController = new BulkLoadReadOnlyController();
-    readOnlyConf = new HBaseConfiguration();
-    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
 
     // mocking variables initialization
     ctx = mock(ObserverContext.class);
@@ -65,23 +60,11 @@ public class TestReadOnlyControllerBulkLoadObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPrePrepareBulkLoadReadOnlyException() throws IOException {
-    bulkLoadReadOnlyController.onConfigurationChange(readOnlyConf);
-    bulkLoadReadOnlyController.prePrepareBulkLoad(ctx);
-  }
-
-  @Test
-  public void testPrePrepareBulkLoadNoException() throws IOException {
     bulkLoadReadOnlyController.prePrepareBulkLoad(ctx);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCleanupBulkLoadReadOnlyException() throws IOException {
-    bulkLoadReadOnlyController.onConfigurationChange(readOnlyConf);
-    bulkLoadReadOnlyController.preCleanupBulkLoad(ctx);
-  }
-
-  @Test
-  public void testPreCleanupBulkLoadNoException() throws IOException {
     bulkLoadReadOnlyController.preCleanupBulkLoad(ctx);
   }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerCoprocessorLoading.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerCoprocessorLoading.java
new file mode 100644
index 00000000000..4846e84bcd6
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerCoprocessorLoading.java
@@ -0,0 +1,258 @@
+/*
+ * 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.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.Collection;
+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.TableName;
+import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.master.HMaster;
+import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
+import org.apache.hadoop.hbase.regionserver.HRegion;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost;
+import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@RunWith(Parameterized.class)
+@Category({ SecurityTests.class, MediumTests.class })
+public class TestReadOnlyControllerCoprocessorLoading {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    
HBaseClassTestRule.forClass(TestReadOnlyControllerCoprocessorLoading.class);
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(TestReadOnlyController.class);
+  private HBaseTestingUtil TEST_UTIL;
+
+  Configuration conf;
+  TableName tableName = TableName.valueOf("test_table");
+  HMaster master;
+  HRegionServer regionServer;
+  HRegion region;
+
+  private final boolean initialReadOnlyMode;
+
+  public TestReadOnlyControllerCoprocessorLoading(boolean initialReadOnlyMode) 
{
+    this.initialReadOnlyMode = initialReadOnlyMode;
+  }
+
+  @Before
+  public void setup() throws Exception {
+    TEST_UTIL = new HBaseTestingUtil();
+    if (TEST_UTIL.getMiniHBaseCluster() != null) {
+      TEST_UTIL.shutdownMiniCluster();
+    }
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    TEST_UTIL.shutdownMiniCluster();
+  }
+
+  private void setupMiniCluster(boolean isReadOnlyEnabled) throws Exception {
+    conf = TEST_UTIL.getConfiguration();
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, 
isReadOnlyEnabled);
+    TEST_UTIL.startMiniCluster(1);
+
+    master = TEST_UTIL.getMiniHBaseCluster().getMaster();
+    regionServer = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
+  }
+
+  private void createTable() throws Exception {
+    // create a table to get at a region
+    TableDescriptor desc = TableDescriptorBuilder.newBuilder(tableName)
+      .setColumnFamily(ColumnFamilyDescriptorBuilder.of("cf")).build();
+    TEST_UTIL.getAdmin().createTable(desc);
+
+    List<HRegion> regions = regionServer.getRegions(tableName);
+    assertFalse(regions.isEmpty());
+    region = regions.get(0);
+  }
+
+  private void setReadOnlyMode(boolean isReadOnlyEnabled) {
+    // Create a new configuration to micic client server behavior
+    // otherwise the existing conf object is shared with the cluster
+    // and can cause side effects on other tests if not reset properly.
+    // This way we can ensure that only the coprocessor loading is tested
+    // without impacting other tests.
+    HBaseTestingUtil NEW_TEST_UTIL = new HBaseTestingUtil();
+    Configuration newConf = NEW_TEST_UTIL.getConfiguration();
+    // Set the read-only enabled config dynamically after cluster startup
+    newConf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, 
isReadOnlyEnabled);
+    master.getConfigurationManager().notifyAllObservers(newConf);
+    regionServer.getConfigurationManager().notifyAllObservers(newConf);
+  }
+
+  private void verifyMasterReadOnlyControllerLoading(boolean 
isReadOnlyEnabled) throws Exception {
+    MasterCoprocessorHost masterCPHost = master.getMasterCoprocessorHost();
+    if (isReadOnlyEnabled) {
+      assertNotNull(
+        MasterReadOnlyController.class.getName()
+          + " should be loaded at startup when readonly is true.",
+        
masterCPHost.findCoprocessor(MasterReadOnlyController.class.getName()));
+    } else {
+      assertNull(
+        MasterReadOnlyController.class.getName()
+          + " should not be loaded at startup when readonly support property 
is false.",
+        
masterCPHost.findCoprocessor(MasterReadOnlyController.class.getName()));
+    }
+  }
+
+  private void verifyRegionServerReadOnlyControllerLoading(boolean 
isReadOnlyEnabled)
+    throws Exception {
+    RegionServerCoprocessorHost rsCPHost = 
regionServer.getRegionServerCoprocessorHost();
+    if (isReadOnlyEnabled) {
+      assertNotNull(
+        RegionServerReadOnlyController.class.getName()
+          + " should be loaded at startup when readonly is true.",
+        
rsCPHost.findCoprocessor(RegionServerReadOnlyController.class.getName()));
+    } else {
+      assertNull(
+        RegionServerReadOnlyController.class.getName()
+          + " should not be loaded at startup when readonly support property 
is false.",
+        
rsCPHost.findCoprocessor(RegionServerReadOnlyController.class.getName()));
+    }
+  }
+
+  private void verifyRegionReadOnlyControllerLoading(boolean 
isReadOnlyEnabled) throws Exception {
+    RegionCoprocessorHost regionCPHost = region.getCoprocessorHost();
+
+    if (isReadOnlyEnabled) {
+      assertNotNull(
+        RegionReadOnlyController.class.getName()
+          + " should be loaded at startup when readonly is true.",
+        
regionCPHost.findCoprocessor(RegionReadOnlyController.class.getName()));
+      assertNotNull(
+        EndpointReadOnlyController.class.getName()
+          + " should be loaded at startup when readonly is true.",
+        
regionCPHost.findCoprocessor(EndpointReadOnlyController.class.getName()));
+      assertNotNull(
+        BulkLoadReadOnlyController.class.getName()
+          + " should be loaded at startup when readonly is true.",
+        
regionCPHost.findCoprocessor(BulkLoadReadOnlyController.class.getName()));
+    } else {
+      assertNull(
+        RegionReadOnlyController.class.getName()
+          + " should not be loaded at startup when readonly support property 
is false",
+        
regionCPHost.findCoprocessor(RegionReadOnlyController.class.getName()));
+      assertNull(
+        EndpointReadOnlyController.class.getName()
+          + " should not be loaded at startup when readonly support property 
is false",
+        
regionCPHost.findCoprocessor(EndpointReadOnlyController.class.getName()));
+      assertNull(
+        BulkLoadReadOnlyController.class.getName()
+          + " should not be loaded at startup when readonly support property 
is false",
+        
regionCPHost.findCoprocessor(BulkLoadReadOnlyController.class.getName()));
+    }
+  }
+
+  private void verifyReadOnlyState(boolean isReadOnlyEnabled) throws Exception 
{
+    verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled);
+    verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled);
+    verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled);
+  }
+
+  @Test
+  public void testReadOnlyControllerStartupBehavior() throws Exception {
+    setupMiniCluster(initialReadOnlyMode);
+    // Table creation is needed to get a region and verify region coprocessor 
loading hence we can't
+    // test region coprocessor loading at startup.
+    // This will get covered in the dynamic loading test where we will also 
verify that the
+    // coprocessors are loaded at after table creation dynamically.
+    verifyMasterReadOnlyControllerLoading(initialReadOnlyMode);
+    verifyRegionServerReadOnlyControllerLoading(initialReadOnlyMode);
+  }
+
+  @Test
+  public void testReadOnlyControllerLoadedWhenEnabledDynamically() throws 
Exception {
+    setupMiniCluster(initialReadOnlyMode);
+    if (!initialReadOnlyMode) {
+      createTable();
+    }
+    boolean isReadOnlyEnabled = true;
+    setReadOnlyMode(isReadOnlyEnabled);
+    verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled);
+    verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled);
+    if (!initialReadOnlyMode) {
+      verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled);
+    }
+  }
+
+  @Test
+  public void testReadOnlyControllerUnloadedWhenDisabledDynamically() throws 
Exception {
+    setupMiniCluster(initialReadOnlyMode);
+    boolean isReadOnlyEnabled = false;
+    setReadOnlyMode(isReadOnlyEnabled);
+    createTable();
+    verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled);
+    verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled);
+    verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled);
+  }
+
+  @Test
+  public void testReadOnlyControllerLoadUnloadedWhenMultipleReadOnlyToggle() 
throws Exception {
+    setupMiniCluster(initialReadOnlyMode);
+
+    // Ensure region exists before validation
+    setReadOnlyMode(false);
+    createTable();
+    verifyReadOnlyState(false);
+
+    // Define toggle sequence
+    boolean[] toggleSequence = new boolean[] { true, false, // basic toggle
+      true, true, // idempotent enable
+      false, false // idempotent disable
+    };
+
+    for (int i = 0; i < toggleSequence.length; i++) {
+      boolean state = toggleSequence[i];
+      LOG.info("Toggling read-only mode to {} (step {})", state, i);
+
+      setReadOnlyMode(state);
+      verifyReadOnlyState(state);
+    }
+  }
+
+  @Parameters(name = "initialReadOnlyMode={0}")
+  public static Collection<Object[]> parameters() {
+    return Arrays.asList(new Object[][] { { Boolean.TRUE }, { Boolean.FALSE } 
});
+  }
+}
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerEndpointObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerEndpointObserver.java
index 2ea46236426..460d1fd2aae 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerEndpointObserver.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerEndpointObserver.java
@@ -17,13 +17,11 @@
  */
 package org.apache.hadoop.hbase.security.access;
 
-import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
-import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.hadoop.hbase.coprocessor.ObserverContext;
 import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
 import org.apache.hadoop.hbase.testclassification.SecurityTests;
@@ -46,8 +44,6 @@ public class TestReadOnlyControllerEndpointObserver {
     HBaseClassTestRule.forClass(TestReadOnlyControllerEndpointObserver.class);
 
   EndpointReadOnlyController endpointReadOnlyController;
-  HBaseConfiguration readOnlyConf;
-
   // Region Server Coprocessor mocking variables.
   ObserverContext<? extends RegionCoprocessorEnvironment> ctx;
   Service service;
@@ -57,8 +53,6 @@ public class TestReadOnlyControllerEndpointObserver {
   @Before
   public void setup() throws Exception {
     endpointReadOnlyController = new EndpointReadOnlyController();
-    readOnlyConf = new HBaseConfiguration();
-    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
 
     // mocking variables initialization
     ctx = mock(ObserverContext.class);
@@ -76,12 +70,6 @@ public class TestReadOnlyControllerEndpointObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreEndpointInvocationReadOnlyException() throws IOException {
-    endpointReadOnlyController.onConfigurationChange(readOnlyConf);
-    endpointReadOnlyController.preEndpointInvocation(ctx, service, methodName, 
request);
-  }
-
-  @Test
-  public void testPreEndpointInvocationNoException() throws IOException {
     endpointReadOnlyController.preEndpointInvocation(ctx, service, methodName, 
request);
   }
 }
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 04f98e9789f..215edb49317 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
@@ -17,7 +17,6 @@
  */
 package org.apache.hadoop.hbase.security.access;
 
-import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
@@ -26,7 +25,6 @@ import java.util.Map;
 import java.util.Set;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
-import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.hadoop.hbase.NamespaceDescriptor;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.client.BalanceRequest;
@@ -59,7 +57,6 @@ public class TestReadOnlyControllerMasterObserver {
     HBaseClassTestRule.forClass(TestReadOnlyControllerMasterObserver.class);
 
   MasterReadOnlyController MasterReadOnlyController;
-  HBaseConfiguration readOnlyConf;
 
   // Master Coprocessor mocking variables
   ObserverContext<MasterCoprocessorEnvironment> c, ctx;
@@ -98,8 +95,6 @@ public class TestReadOnlyControllerMasterObserver {
   @Before
   public void setup() throws Exception {
     MasterReadOnlyController = new MasterReadOnlyController();
-    readOnlyConf = new HBaseConfiguration();
-    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
 
     // mocking variables initialization
     c = mock(ObserverContext.class);
@@ -152,541 +147,247 @@ public class TestReadOnlyControllerMasterObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCreateTableRegionsInfosReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preCreateTableRegionsInfos(ctx, desc);
-  }
-
-  @Test
-  public void testPreCreateTableRegionsInfosNoException() throws IOException {
     MasterReadOnlyController.preCreateTableRegionsInfos(ctx, desc);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCreateTableReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preCreateTable(ctx, desc, regions);
-  }
-
-  @Test
-  public void testPreCreateTableNoException() throws IOException {
     MasterReadOnlyController.preCreateTable(ctx, desc, regions);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCreateTableActionReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preCreateTableAction(ctx, desc, regions);
-  }
-
-  @Test
-  public void testPreCreateTableActionNoException() throws IOException {
     MasterReadOnlyController.preCreateTableAction(ctx, desc, regions);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreDeleteTableReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preDeleteTable(ctx, tableName);
-  }
-
-  @Test
-  public void testPreDeleteTableNoException() throws IOException {
     MasterReadOnlyController.preDeleteTable(ctx, tableName);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreDeleteTableActionReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preDeleteTableAction(ctx, tableName);
-  }
-
-  @Test
-  public void testPreDeleteTableActionNoException() throws IOException {
     MasterReadOnlyController.preDeleteTableAction(ctx, tableName);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreTruncateTableReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preTruncateTable(ctx, tableName);
-  }
-
-  @Test
-  public void testPreTruncateTableNoException() throws IOException {
     MasterReadOnlyController.preTruncateTable(ctx, tableName);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreTruncateTableActionReadOnlyException() throws IOException 
{
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preTruncateTableAction(ctx, tableName);
-  }
-
-  @Test
-  public void testPreTruncateTableActionNoException() throws IOException {
     MasterReadOnlyController.preTruncateTableAction(ctx, tableName);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreModifyTableReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preModifyTable(ctx, tableName, currentDescriptor, 
newDescriptor);
-  }
-
-  @Test
-  public void testPreModifyTableNoException() throws IOException {
     MasterReadOnlyController.preModifyTable(ctx, tableName, currentDescriptor, 
newDescriptor);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreModifyTableStoreFileTrackerReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preModifyTableStoreFileTracker(ctx, tableName, 
dstSFT);
-  }
-
-  @Test
-  public void testPreModifyTableStoreFileTrackerNoException() throws 
IOException {
     MasterReadOnlyController.preModifyTableStoreFileTracker(ctx, tableName, 
dstSFT);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreModifyColumnFamilyStoreFileTrackerReadOnlyException() 
throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preModifyColumnFamilyStoreFileTracker(ctx, 
tableName, family, dstSFT);
-  }
-
-  @Test
-  public void testPreModifyColumnFamilyStoreFileTrackerNoException() throws 
IOException {
     MasterReadOnlyController.preModifyColumnFamilyStoreFileTracker(ctx, 
tableName, family, dstSFT);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreModifyTableActionReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preModifyTableAction(ctx, tableName, 
currentDescriptor, newDescriptor);
-  }
-
-  @Test
-  public void testPreModifyTableActionNoException() throws IOException {
     MasterReadOnlyController.preModifyTableAction(ctx, tableName, 
currentDescriptor, newDescriptor);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSplitRegionReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSplitRegion(c, tableName, splitRow);
-  }
-
-  @Test
-  public void testPreSplitRegionNoException() throws IOException {
     MasterReadOnlyController.preSplitRegion(c, tableName, splitRow);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSplitRegionActionReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSplitRegionAction(c, tableName, splitRow);
-  }
-
-  @Test
-  public void testPreSplitRegionActionNoException() throws IOException {
     MasterReadOnlyController.preSplitRegionAction(c, tableName, splitRow);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSplitRegionBeforeMETAActionReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSplitRegionBeforeMETAAction(ctx, splitKey, 
metaEntries);
-  }
-
-  @Test
-  public void testPreSplitRegionBeforeMETAActionNoException() throws 
IOException {
     MasterReadOnlyController.preSplitRegionBeforeMETAAction(ctx, splitKey, 
metaEntries);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSplitRegionAfterMETAActionReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSplitRegionAfterMETAAction(ctx);
-  }
-
-  @Test
-  public void testPreSplitRegionAfterMETAActionNoException() throws 
IOException {
     MasterReadOnlyController.preSplitRegionAfterMETAAction(ctx);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMergeRegionsActionReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preMergeRegionsAction(ctx, regionsToMerge);
-  }
-
-  @Test
-  public void testPreMergeRegionsActionNoException() throws IOException {
     MasterReadOnlyController.preMergeRegionsAction(ctx, regionsToMerge);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMergeRegionsCommitActionReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preMergeRegionsCommitAction(ctx, regionsToMerge, 
metaEntries);
-  }
-
-  @Test
-  public void testPreMergeRegionsCommitActionNoException() throws IOException {
     MasterReadOnlyController.preMergeRegionsCommitAction(ctx, regionsToMerge, 
metaEntries);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSnapshotReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSnapshot(ctx, snapshot, tableDescriptor);
-  }
-
-  @Test
-  public void testPreSnapshotNoException() throws IOException {
     MasterReadOnlyController.preSnapshot(ctx, snapshot, tableDescriptor);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCloneSnapshotReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preCloneSnapshot(ctx, snapshot, tableDescriptor);
-  }
-
-  @Test
-  public void testPreCloneSnapshotNoException() throws IOException {
     MasterReadOnlyController.preCloneSnapshot(ctx, snapshot, tableDescriptor);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreRestoreSnapshotReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preRestoreSnapshot(ctx, snapshot, 
tableDescriptor);
-  }
-
-  @Test
-  public void testPreRestoreSnapshotNoException() throws IOException {
     MasterReadOnlyController.preRestoreSnapshot(ctx, snapshot, 
tableDescriptor);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreDeleteSnapshotReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preDeleteSnapshot(ctx, snapshot);
-  }
-
-  @Test
-  public void testPreDeleteSnapshotNoException() throws IOException {
     MasterReadOnlyController.preDeleteSnapshot(ctx, snapshot);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCreateNamespaceReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preCreateNamespace(ctx, ns);
-  }
-
-  @Test
-  public void testPreCreateNamespaceNoException() throws IOException {
     MasterReadOnlyController.preCreateNamespace(ctx, ns);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreModifyNamespaceReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preModifyNamespace(ctx, currentNsDescriptor, 
newNsDescriptor);
-  }
-
-  @Test
-  public void testPreModifyNamespaceNoException() throws IOException {
     MasterReadOnlyController.preModifyNamespace(ctx, currentNsDescriptor, 
newNsDescriptor);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreDeleteNamespaceReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preDeleteNamespace(ctx, namespace);
-  }
-
-  @Test
-  public void testPreDeleteNamespaceNoException() throws IOException {
     MasterReadOnlyController.preDeleteNamespace(ctx, namespace);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMasterStoreFlushReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preMasterStoreFlush(ctx);
-  }
-
-  @Test
-  public void testPreMasterStoreFlushNoException() throws IOException {
     MasterReadOnlyController.preMasterStoreFlush(ctx);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSetUserQuotaReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSetUserQuota(ctx, userName, quotas);
-  }
-
-  @Test
-  public void testPreSetUserQuotaNoException() throws IOException {
     MasterReadOnlyController.preSetUserQuota(ctx, userName, quotas);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSetUserQuotaOnTableReadOnlyException() throws IOException 
{
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSetUserQuota(ctx, userName, tableName, quotas);
-  }
-
-  @Test
-  public void testPreSetUserQuotaOnTableNoException() throws IOException {
     MasterReadOnlyController.preSetUserQuota(ctx, userName, tableName, quotas);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSetUserQuotaOnNamespaceReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSetUserQuota(ctx, userName, namespace, quotas);
-  }
-
-  @Test
-  public void testPreSetUserQuotaOnNamespaceNoException() throws IOException {
     MasterReadOnlyController.preSetUserQuota(ctx, userName, namespace, quotas);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSetTableQuotaReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSetTableQuota(ctx, tableName, quotas);
-  }
-
-  @Test
-  public void testPreSetTableQuotaNoException() throws IOException {
     MasterReadOnlyController.preSetTableQuota(ctx, tableName, quotas);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSetNamespaceQuotaReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSetNamespaceQuota(ctx, namespace, quotas);
-  }
-
-  @Test
-  public void testPreSetNamespaceQuotaNoException() throws IOException {
     MasterReadOnlyController.preSetNamespaceQuota(ctx, namespace, quotas);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreSetRegionServerQuotaReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preSetRegionServerQuota(ctx, regionServer, 
quotas);
-  }
-
-  @Test
-  public void testPreSetRegionServerQuotaNoException() throws IOException {
     MasterReadOnlyController.preSetRegionServerQuota(ctx, regionServer, 
quotas);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMergeRegionsReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preMergeRegions(ctx, regionsToMerge);
-  }
-
-  @Test
-  public void testPreMergeRegionsNoException() throws IOException {
     MasterReadOnlyController.preMergeRegions(ctx, regionsToMerge);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMoveServersAndTablesReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preMoveServersAndTables(ctx, servers, tables, 
targetGroup);
-  }
-
-  @Test
-  public void testPreMoveServersAndTablesNoException() throws IOException {
     MasterReadOnlyController.preMoveServersAndTables(ctx, servers, tables, 
targetGroup);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMoveServersReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preMoveServers(ctx, servers, targetGroup);
-  }
-
-  @Test
-  public void testPreMoveServersNoException() throws IOException {
     MasterReadOnlyController.preMoveServers(ctx, servers, targetGroup);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMoveTablesReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preMoveTables(ctx, tables, targetGroup);
-  }
-
-  @Test
-  public void testPreMoveTablesNoException() throws IOException {
     MasterReadOnlyController.preMoveTables(ctx, tables, targetGroup);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreAddRSGroupReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preAddRSGroup(ctx, name);
-  }
-
-  @Test
-  public void testPreAddRSGroupNoException() throws IOException {
     MasterReadOnlyController.preAddRSGroup(ctx, name);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreRemoveRSGroupReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preRemoveRSGroup(ctx, name);
-  }
-
-  @Test
-  public void testPreRemoveRSGroupNoException() throws IOException {
     MasterReadOnlyController.preRemoveRSGroup(ctx, name);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreBalanceRSGroupReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preBalanceRSGroup(ctx, groupName, request);
-  }
-
-  @Test
-  public void testPreBalanceRSGroupNoException() throws IOException {
     MasterReadOnlyController.preBalanceRSGroup(ctx, groupName, request);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreRemoveServersReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preRemoveServers(ctx, servers);
-  }
-
-  @Test
-  public void testPreRemoveServersNoException() throws IOException {
     MasterReadOnlyController.preRemoveServers(ctx, servers);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreRenameRSGroupReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preRenameRSGroup(ctx, oldName, newName);
-  }
-
-  @Test
-  public void testPreRenameRSGroupNoException() throws IOException {
     MasterReadOnlyController.preRenameRSGroup(ctx, oldName, newName);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreUpdateRSGroupConfigReadOnlyException() throws IOException 
{
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preUpdateRSGroupConfig(ctx, groupName, 
configuration);
-  }
-
-  @Test
-  public void testPreUpdateRSGroupConfigNoException() throws IOException {
     MasterReadOnlyController.preUpdateRSGroupConfig(ctx, groupName, 
configuration);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreAddReplicationPeerReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preAddReplicationPeer(ctx, peerId, peerConfig);
-  }
-
-  @Test
-  public void testPreAddReplicationPeerNoException() throws IOException {
     MasterReadOnlyController.preAddReplicationPeer(ctx, peerId, peerConfig);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreRemoveReplicationPeerReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preRemoveReplicationPeer(ctx, peerId);
-  }
-
-  @Test
-  public void testPreRemoveReplicationPeerNoException() throws IOException {
     MasterReadOnlyController.preRemoveReplicationPeer(ctx, peerId);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreEnableReplicationPeerReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preEnableReplicationPeer(ctx, peerId);
-  }
-
-  @Test
-  public void testPreEnableReplicationPeerNoException() throws IOException {
     MasterReadOnlyController.preEnableReplicationPeer(ctx, peerId);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreDisableReplicationPeerReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preDisableReplicationPeer(ctx, peerId);
-  }
-
-  @Test
-  public void testPreDisableReplicationPeerNoException() throws IOException {
     MasterReadOnlyController.preDisableReplicationPeer(ctx, peerId);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreUpdateReplicationPeerConfigReadOnlyException() throws 
IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preUpdateReplicationPeerConfig(ctx, peerId, 
peerConfig);
-  }
-
-  @Test
-  public void testPreUpdateReplicationPeerConfigNoException() throws 
IOException {
     MasterReadOnlyController.preUpdateReplicationPeerConfig(ctx, peerId, 
peerConfig);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void 
testPreTransitReplicationPeerSyncReplicationStateReadOnlyException()
     throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    
MasterReadOnlyController.preTransitReplicationPeerSyncReplicationState(ctx, 
peerId, state);
-  }
-
-  @Test
-  public void testPreTransitReplicationPeerSyncReplicationStateNoException() 
throws IOException {
     
MasterReadOnlyController.preTransitReplicationPeerSyncReplicationState(ctx, 
peerId, state);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreGrantReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preGrant(ctx, userPermission, 
mergeExistingPermissions);
-  }
-
-  @Test
-  public void testPreGrantNoException() throws IOException {
     MasterReadOnlyController.preGrant(ctx, userPermission, 
mergeExistingPermissions);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreRevokeReadOnlyException() throws IOException {
-    MasterReadOnlyController.onConfigurationChange(readOnlyConf);
-    MasterReadOnlyController.preRevoke(ctx, userPermission);
-  }
-
-  @Test
-  public void testPreRevokeNoException() throws IOException {
     MasterReadOnlyController.preRevoke(ctx, userPermission);
   }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionObserver.java
index 33f9a337cbe..b2e53860124 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionObserver.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionObserver.java
@@ -17,7 +17,6 @@
  */
 package org.apache.hadoop.hbase.security.access;
 
-import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -27,7 +26,6 @@ import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.CompareOperator;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
-import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.client.Append;
 import org.apache.hadoop.hbase.client.CheckAndMutate;
@@ -69,7 +67,6 @@ import org.junit.experimental.categories.Category;
 // For example, prePut has 2 versions:
 // V1: prePut(ObserverContext<RegionCoprocessorEnvironment> c, Put put, 
WALEdit edit)
 // V2: prePut(ObserverContext<RegionCoprocessorEnvironment> c, Put put, 
WALEdit edit, Durability durability)
-
 @Category({ SecurityTests.class, SmallTests.class })
 public class TestReadOnlyControllerRegionObserver {
   @ClassRule
@@ -77,7 +74,6 @@ public class TestReadOnlyControllerRegionObserver {
     HBaseClassTestRule.forClass(TestReadOnlyControllerRegionObserver.class);
 
   RegionReadOnlyController regionReadOnlyController;
-  HBaseConfiguration readOnlyConf;
 
   // Region Coprocessor mocking variables
   ObserverContext<RegionCoprocessorEnvironment> c, ctx;
@@ -117,8 +113,6 @@ public class TestReadOnlyControllerRegionObserver {
   @Before
   public void setup() throws Exception {
     regionReadOnlyController = new RegionReadOnlyController();
-    readOnlyConf = new HBaseConfiguration();
-    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
 
     // mocking variables initialization
     c = mock(ObserverContext.class);
@@ -183,156 +177,71 @@ public class TestReadOnlyControllerRegionObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreFlushV1ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preFlush(c, flushLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreFlushV1NoException() throws IOException {
     regionReadOnlyController.preFlush(c, flushLifeCycleTracker);
   }
 
   @Test
   public void testPreFlushV1ReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preFlush(c, flushLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreFlushV1MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preFlush(c, flushLifeCycleTracker);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreFlushV2ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preFlush(c, store, scanner, 
flushLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreFlushV2NoException() throws IOException {
     regionReadOnlyController.preFlush(c, store, scanner, 
flushLifeCycleTracker);
   }
 
   @Test
   public void testPreFlushV2ReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preFlush(c, store, scanner, 
flushLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreFlushV2MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preFlush(c, store, scanner, 
flushLifeCycleTracker);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreFlushScannerOpenReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preFlushScannerOpen(c, store, options, 
flushLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreFlushScannerOpenNoException() throws IOException {
     regionReadOnlyController.preFlushScannerOpen(c, store, options, 
flushLifeCycleTracker);
   }
 
   @Test
   public void testPreFlushScannerOpenReadOnlyMetaNoException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preFlushScannerOpen(c, store, options, 
flushLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreFlushScannerOpenMetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preFlushScannerOpen(c, store, options, 
flushLifeCycleTracker);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMemStoreCompactionReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preMemStoreCompaction(c, store);
-  }
-
-  @Test
-  public void testPreMemStoreCompactionNoException() throws IOException {
     regionReadOnlyController.preMemStoreCompaction(c, store);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMemStoreCompactionCompactScannerOpenReadOnlyException() 
throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preMemStoreCompactionCompactScannerOpen(c, store, 
options);
-  }
-
-  @Test
-  public void testPreMemStoreCompactionCompactScannerOpenNoException() throws 
IOException {
     regionReadOnlyController.preMemStoreCompactionCompactScannerOpen(c, store, 
options);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreMemStoreCompactionCompactReadOnlyException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preMemStoreCompactionCompact(c, store, scanner);
-  }
-
-  @Test
-  public void testPreMemStoreCompactionCompactNoException() throws IOException 
{
     regionReadOnlyController.preMemStoreCompactionCompact(c, store, scanner);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCompactSelectionReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCompactSelection(c, store, candidates, 
compactionLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreCompactSelectionNoException() throws IOException {
     regionReadOnlyController.preCompactSelection(c, store, candidates, 
compactionLifeCycleTracker);
   }
 
   @Test
   public void testPreCompactSelectionReadOnlyMetaNoException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCompactSelection(c, store, candidates, 
compactionLifeCycleTracker);
-  }
-
-  @Test
-  public void testPreCompactSelectionMetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCompactSelection(c, store, candidates, 
compactionLifeCycleTracker);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCompactScannerOpenReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCompactScannerOpen(c, store, scanType, options,
-      compactionLifeCycleTracker, compactionRequest);
-  }
-
-  @Test
-  public void testPreCompactScannerOpenNoException() throws IOException {
     regionReadOnlyController.preCompactScannerOpen(c, store, scanType, options,
       compactionLifeCycleTracker, compactionRequest);
   }
 
   @Test
   public void testPreCompactScannerOpenReadOnlyMetaNoException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCompactScannerOpen(c, store, scanType, options,
-      compactionLifeCycleTracker, compactionRequest);
-  }
-
-  @Test
-  public void testPreCompactScannerOpenMetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCompactScannerOpen(c, store, scanType, options,
       compactionLifeCycleTracker, compactionRequest);
@@ -340,27 +249,12 @@ public class TestReadOnlyControllerRegionObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCompactReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCompact(c, store, scanner, scanType, 
compactionLifeCycleTracker,
-      compactionRequest);
-  }
-
-  @Test
-  public void testPreCompactNoException() throws IOException {
     regionReadOnlyController.preCompact(c, store, scanner, scanType, 
compactionLifeCycleTracker,
       compactionRequest);
   }
 
   @Test
   public void testPreCompactReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCompact(c, store, scanner, scanType, 
compactionLifeCycleTracker,
-      compactionRequest);
-  }
-
-  @Test
-  public void testPreCompactMetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCompact(c, store, scanner, scanType, 
compactionLifeCycleTracker,
       compactionRequest);
@@ -368,195 +262,89 @@ public class TestReadOnlyControllerRegionObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPrePutV1ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.prePut(c, put, edit);
-  }
-
-  @Test
-  public void testPrePutV1NoException() throws IOException {
     regionReadOnlyController.prePut(c, put, edit);
   }
 
   @Test
   public void testPrePutV1ReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.prePut(c, put, edit);
-  }
-
-  @Test
-  public void testPrePutV1MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.prePut(c, put, edit);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPrePutV2ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.prePut(c, put, edit, durability);
-  }
-
-  @Test
-  public void testPrePutV2NoException() throws IOException {
     regionReadOnlyController.prePut(c, put, edit, durability);
   }
 
   @Test
   public void testPrePutV2ReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.prePut(c, put, edit, durability);
-  }
-
-  @Test
-  public void testPrePutV2MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.prePut(c, put, edit, durability);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreDeleteV1ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preDelete(c, delete, edit);
-  }
-
-  @Test
-  public void testPreDeleteV1NoException() throws IOException {
     regionReadOnlyController.preDelete(c, delete, edit);
   }
 
   @Test
   public void testPreDeleteV1ReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preDelete(c, delete, edit);
-  }
-
-  @Test
-  public void testPreDeleteV1MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preDelete(c, delete, edit);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreDeleteV2ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preDelete(c, delete, edit, durability);
-  }
-
-  @Test
-  public void testPreDeleteV2NoException() throws IOException {
     regionReadOnlyController.preDelete(c, delete, edit, durability);
   }
 
   @Test
   public void testPreDeleteV2ReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preDelete(c, delete, edit, durability);
-  }
-
-  @Test
-  public void testPreDeleteV2MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preDelete(c, delete, edit, durability);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreBatchMutateReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preBatchMutate(c, miniBatchOp);
-  }
-
-  @Test
-  public void testPreBatchMutateNoException() throws IOException {
     regionReadOnlyController.preBatchMutate(c, miniBatchOp);
   }
 
   @Test
   public void testPreBatchMutateReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preBatchMutate(c, miniBatchOp);
-  }
-
-  @Test
-  public void testPreBatchMutateMetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preBatchMutate(c, miniBatchOp);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndPutV1ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndPut(c, row, family, qualifier, op, 
comparator, put, result);
-  }
-
-  @Test
-  public void testPreCheckAndPutV1NoException() throws IOException {
     regionReadOnlyController.preCheckAndPut(c, row, family, qualifier, op, 
comparator, put, result);
   }
 
   @Test
   public void testPreCheckAndPutV1ReadOnlyMetaNoException() throws IOException 
{
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndPut(c, row, family, qualifier, op, 
comparator, put, result);
-  }
-
-  @Test
-  public void testPreCheckAndPutV1MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndPut(c, row, family, qualifier, op, 
comparator, put, result);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndPutV2ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndPut(c, row, filter, put, result);
-  }
-
-  @Test
-  public void testPreCheckAndPutV2NoException() throws IOException {
     regionReadOnlyController.preCheckAndPut(c, row, filter, put, result);
   }
 
   @Test
   public void testPreCheckAndPutV2ReadOnlyMetaNoException() throws IOException 
{
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndPut(c, row, filter, put, result);
-  }
-
-  @Test
-  public void testPreCheckAndPutV2MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndPut(c, row, filter, put, result);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndPutAfterRowLockV1ReadOnlyException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, family, 
qualifier, op, comparator,
-      put, result);
-  }
-
-  @Test
-  public void testPreCheckAndPutAfterRowLockV1NoException() throws IOException 
{
     regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, family, 
qualifier, op, comparator,
       put, result);
   }
 
   @Test
   public void testPreCheckAndPutAfterRowLockV1ReadOnlyMetaNoException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, family, 
qualifier, op, comparator,
-      put, result);
-  }
-
-  @Test
-  public void testPreCheckAndPutAfterRowLockV1MetaNoException() throws 
IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, family, 
qualifier, op, comparator,
       put, result);
@@ -564,51 +352,23 @@ public class TestReadOnlyControllerRegionObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndPutAfterRowLockV2ReadOnlyException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, filter, put, 
result);
-  }
-
-  @Test
-  public void testPreCheckAndPutAfterRowLockV2NoException() throws IOException 
{
     regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, filter, put, 
result);
   }
 
   @Test
   public void testPreCheckAndPutAfterRowLockV2ReadOnlyMetaNoException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, filter, put, 
result);
-  }
-
-  @Test
-  public void testPreCheckAndPutAfterRowLockV2MetaNoException() throws 
IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndPutAfterRowLock(c, row, filter, put, 
result);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndDeleteV1ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndDelete(c, row, family, qualifier, op, 
comparator, delete,
-      result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteV1NoException() throws IOException {
     regionReadOnlyController.preCheckAndDelete(c, row, family, qualifier, op, 
comparator, delete,
       result);
   }
 
   @Test
   public void testPreCheckAndDeleteV1ReadOnlyMetaNoException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndDelete(c, row, family, qualifier, op, 
comparator, delete,
-      result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteV1MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndDelete(c, row, family, qualifier, op, 
comparator, delete,
       result);
@@ -616,51 +376,23 @@ public class TestReadOnlyControllerRegionObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndDeleteV2ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndDelete(c, row, filter, delete, result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteV2NoException() throws IOException {
     regionReadOnlyController.preCheckAndDelete(c, row, filter, delete, result);
   }
 
   @Test
   public void testPreCheckAndDeleteV2ReadOnlyMetaNoException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndDelete(c, row, filter, delete, result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteV2MetaNoException() throws IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndDelete(c, row, filter, delete, result);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndDeleteAfterRowLockV1ReadOnlyException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, family, 
qualifier, op,
-      comparator, delete, result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteAfterRowLockV1NoException() throws 
IOException {
     regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, family, 
qualifier, op,
       comparator, delete, result);
   }
 
   @Test
   public void testPreCheckAndDeleteAfterRowLockV1ReadOnlyMetaNoException() 
throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, family, 
qualifier, op,
-      comparator, delete, result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteAfterRowLockV1MetaNoException() throws 
IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, family, 
qualifier, op,
       comparator, delete, result);
@@ -668,169 +400,77 @@ public class TestReadOnlyControllerRegionObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndDeleteAfterRowLockV2ReadOnlyException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, filter, 
delete, result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteAfterRowLockV2NoException() throws 
IOException {
     regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, filter, 
delete, result);
   }
 
   @Test
   public void testPreCheckAndDeleteAfterRowLockV2ReadOnlyMetaNoException() 
throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    mockOperationForMetaTable();
-    regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, filter, 
delete, result);
-  }
-
-  @Test
-  public void testPreCheckAndDeleteAfterRowLockV2MetaNoException() throws 
IOException {
     mockOperationForMetaTable();
     regionReadOnlyController.preCheckAndDeleteAfterRowLock(c, row, filter, 
delete, result);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndMutateReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndMutate(c, checkAndMutate, 
checkAndMutateResult);
-  }
-
-  @Test
-  public void testPreCheckAndMutateNoException() throws IOException {
     regionReadOnlyController.preCheckAndMutate(c, checkAndMutate, 
checkAndMutateResult);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCheckAndMutateAfterRowLockReadOnlyException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCheckAndMutateAfterRowLock(c, checkAndMutate, 
checkAndMutateResult);
-  }
-
-  @Test
-  public void testPreCheckAndMutateAfterRowLockNoException() throws 
IOException {
     regionReadOnlyController.preCheckAndMutateAfterRowLock(c, checkAndMutate, 
checkAndMutateResult);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreAppendV1ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preAppend(c, append);
-  }
-
-  @Test
-  public void testPreAppendV1NoException() throws IOException {
     regionReadOnlyController.preAppend(c, append);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreAppendV2ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preAppend(c, append, edit);
-  }
-
-  @Test
-  public void testPreAppendV2NoException() throws IOException {
     regionReadOnlyController.preAppend(c, append, edit);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreAppendAfterRowLockReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preAppendAfterRowLock(c, append);
-  }
-
-  @Test
-  public void testPreAppendAfterRowLockNoException() throws IOException {
     regionReadOnlyController.preAppendAfterRowLock(c, append);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreIncrementV1ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preIncrement(c, increment);
-  }
-
-  @Test
-  public void testPreIncrementV1NoException() throws IOException {
     regionReadOnlyController.preIncrement(c, increment);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreIncrementV2ReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preIncrement(c, increment, edit);
-  }
-
-  @Test
-  public void testPreIncrementV2NoException() throws IOException {
     regionReadOnlyController.preIncrement(c, increment, edit);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreIncrementAfterRowLockReadOnlyException() throws 
IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preIncrementAfterRowLock(c, increment);
-  }
-
-  @Test
-  public void testPreIncrementAfterRowLockNoException() throws IOException {
     regionReadOnlyController.preIncrementAfterRowLock(c, increment);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreReplayWALsReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preReplayWALs(ctx, info, edits);
-  }
-
-  @Test
-  public void testPreReplayWALsNoException() throws IOException {
     regionReadOnlyController.preReplayWALs(ctx, info, edits);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreBulkLoadHFileReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preBulkLoadHFile(ctx, familyPaths);
-  }
-
-  @Test
-  public void testPreBulkLoadHFileNoException() throws IOException {
     regionReadOnlyController.preBulkLoadHFile(ctx, familyPaths);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreCommitStoreFileReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preCommitStoreFile(ctx, family, pairs);
-  }
-
-  @Test
-  public void testPreCommitStoreFileNoException() throws IOException {
     regionReadOnlyController.preCommitStoreFile(ctx, family, pairs);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreWALAppendReadOnlyException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionReadOnlyController.preWALAppend(ctx, key, edit);
-  }
-
-  @Test
-  public void testPreWALAppendNoException() throws IOException {
     regionReadOnlyController.preWALAppend(ctx, key, edit);
   }
 
   @Test
   public void testPreWALAppendReadOnlyMetaNoException() throws IOException {
-    regionReadOnlyController.onConfigurationChange(readOnlyConf);
-    when(key.getTableName()).thenReturn(TableName.META_TABLE_NAME);
-    regionReadOnlyController.preWALAppend(ctx, key, edit);
-  }
-
-  @Test
-  public void testPreWALAppendMetaNoException() throws IOException {
     when(key.getTableName()).thenReturn(TableName.META_TABLE_NAME);
     regionReadOnlyController.preWALAppend(ctx, key, edit);
   }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionServerObserver.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionServerObserver.java
index 57424085504..382e55a0c40 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionServerObserver.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyControllerRegionServerObserver.java
@@ -17,13 +17,11 @@
  */
 package org.apache.hadoop.hbase.security.access;
 
-import static 
org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
 import static org.mockito.Mockito.mock;
 
 import java.io.IOException;
 import org.apache.hadoop.hbase.DoNotRetryIOException;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
-import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.hadoop.hbase.client.Mutation;
 import org.apache.hadoop.hbase.coprocessor.ObserverContext;
 import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessorEnvironment;
@@ -49,7 +47,6 @@ public class TestReadOnlyControllerRegionServerObserver {
     
HBaseClassTestRule.forClass(TestReadOnlyControllerRegionServerObserver.class);
 
   RegionServerReadOnlyController regionServerReadOnlyController;
-  HBaseConfiguration readOnlyConf;
 
   // Region Server Coprocessor mocking variables
   ObserverContext<RegionServerCoprocessorEnvironment> ctx;
@@ -59,8 +56,6 @@ public class TestReadOnlyControllerRegionServerObserver {
   @Before
   public void setup() throws Exception {
     regionServerReadOnlyController = new RegionServerReadOnlyController();
-    readOnlyConf = new HBaseConfiguration();
-    readOnlyConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
 
     // mocking variables initialization
     ctx = mock(ObserverContext.class);
@@ -79,34 +74,16 @@ public class TestReadOnlyControllerRegionServerObserver {
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreRollWALWriterRequestReadOnlyException() throws 
IOException {
-    regionServerReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionServerReadOnlyController.preRollWALWriterRequest(ctx);
-  }
-
-  @Test
-  public void testPreRollWALWriterRequestNoException() throws IOException {
     regionServerReadOnlyController.preRollWALWriterRequest(ctx);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreReplicationSinkBatchMutateReadOnlyException() throws 
IOException {
-    regionServerReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionServerReadOnlyController.preReplicationSinkBatchMutate(ctx, 
walEntry, mutation);
-  }
-
-  @Test
-  public void testPreReplicationSinkBatchMutateNoException() throws 
IOException {
     regionServerReadOnlyController.preReplicationSinkBatchMutate(ctx, 
walEntry, mutation);
   }
 
   @Test(expected = DoNotRetryIOException.class)
   public void testPreReplicateLogEntriesReadOnlyException() throws IOException 
{
-    regionServerReadOnlyController.onConfigurationChange(readOnlyConf);
-    regionServerReadOnlyController.preReplicateLogEntries(ctx);
-  }
-
-  @Test
-  public void testPreReplicateLogEntriesNoException() throws IOException {
     regionServerReadOnlyController.preReplicateLogEntries(ctx);
   }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestCoprocessorConfigurationUtil.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestCoprocessorConfigurationUtil.java
new file mode 100644
index 00000000000..eeda7194982
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestCoprocessorConfigurationUtil.java
@@ -0,0 +1,200 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
+import org.apache.hadoop.hbase.security.access.BulkLoadReadOnlyController;
+import org.apache.hadoop.hbase.security.access.EndpointReadOnlyController;
+import org.apache.hadoop.hbase.security.access.MasterReadOnlyController;
+import org.apache.hadoop.hbase.security.access.RegionReadOnlyController;
+import org.apache.hadoop.hbase.security.access.RegionServerReadOnlyController;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestCoprocessorConfigurationUtil {
+
+  private Configuration conf;
+  private String key;
+
+  @Before
+  public void setUp() {
+    conf = new Configuration();
+    key = "test.key";
+  }
+
+  @Test
+  public void testAddCoprocessorsEmptyCPList() {
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp1", 
"cp2"));
+    assertArrayEquals(new String[] { "cp1", "cp2" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testAddCoprocessorsNonEmptyCPList() {
+    conf.setStrings(key, "cp1");
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp1", 
"cp2"));
+    assertArrayEquals(new String[] { "cp1", "cp2" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testAddCoprocessorsNoChange() {
+    conf.setStrings(key, "cp1");
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp1"));
+    assertArrayEquals(new String[] { "cp1" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testAddCoprocessorsIdempotent() {
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp1", 
"cp2"));
+    // Call again
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp1", 
"cp2"));
+    assertArrayEquals(new String[] { "cp1", "cp2" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testAddCoprocessorsIdempotentWithOverlap() {
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp1"));
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp1", 
"cp2"));
+    CoprocessorConfigurationUtil.addCoprocessors(conf, key, List.of("cp2"));
+    assertArrayEquals(new String[] { "cp1", "cp2" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testRemoveCoprocessorsEmptyCPList() {
+    CoprocessorConfigurationUtil.removeCoprocessors(conf, key, List.of("cp1"));
+    assertNull(conf.getStrings(key));
+  }
+
+  @Test
+  public void testRemoveCoprocessorsNonEmptyCPList() {
+    conf.setStrings(key, "cp1", "cp2", "cp3");
+    CoprocessorConfigurationUtil.removeCoprocessors(conf, key, List.of("cp2"));
+    assertArrayEquals(new String[] { "cp1", "cp3" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testRemoveCoprocessorsNoChange() {
+    conf.setStrings(key, "cp1");
+    CoprocessorConfigurationUtil.removeCoprocessors(conf, key, List.of("cp2"));
+    assertArrayEquals(new String[] { "cp1" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testRemoveCoprocessorsIdempotent() {
+    conf.setStrings(key, "cp1", "cp2");
+    CoprocessorConfigurationUtil.removeCoprocessors(conf, key, List.of("cp2"));
+    // Call again
+    CoprocessorConfigurationUtil.removeCoprocessors(conf, key, List.of("cp2"));
+    assertArrayEquals(new String[] { "cp1" }, conf.getStrings(key));
+  }
+
+  @Test
+  public void testRemoveCoprocessorsIdempotentWhenNotPresent() {
+    conf.setStrings(key, "cp1");
+    CoprocessorConfigurationUtil.removeCoprocessors(conf, key, List.of("cp2"));
+    CoprocessorConfigurationUtil.removeCoprocessors(conf, key, List.of("cp2"));
+    assertArrayEquals(new String[] { "cp1" }, conf.getStrings(key));
+  }
+
+  private void assertEnable(String key, List<String> expected) {
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(true, conf, key);
+    assertTrue(conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, 
false));
+    String[] result = conf.getStrings(key);
+    assertNotNull(result);
+    assertEquals(expected.size(), result.length);
+    assertTrue(Arrays.asList(result).containsAll(expected));
+  }
+
+  @Test
+  public void testSyncReadOnlyConfigurationsReadOnlyEnableAllKeys() {
+    assertEnable(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
+      List.of(MasterReadOnlyController.class.getName()));
+
+    assertEnable(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
+      List.of(RegionServerReadOnlyController.class.getName()));
+    assertEnable(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
+      List.of(RegionReadOnlyController.class.getName(), 
BulkLoadReadOnlyController.class.getName(),
+        EndpointReadOnlyController.class.getName()));
+  }
+
+  private void assertDisable(String key, List<String> initialCoprocs) {
+    conf.setStrings(key, initialCoprocs.toArray(new String[0]));
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(false, conf, key);
+    assertFalse(conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, 
true));
+    String[] result = conf.getStrings(key);
+    assertTrue(result == null || result.length == 0);
+  }
+
+  @Test
+  public void testSyncReadOnlyConfigurationsReadOnlyDisableAllKeys() {
+    assertDisable(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
+      List.of(MasterReadOnlyController.class.getName()));
+
+    assertDisable(CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
+      List.of(RegionServerReadOnlyController.class.getName()));
+
+    assertDisable(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
+      List.of(RegionReadOnlyController.class.getName(), 
BulkLoadReadOnlyController.class.getName(),
+        EndpointReadOnlyController.class.getName()));
+  }
+
+  @Test
+  public void 
testSyncReadOnlyConfigurationsReadOnlyEnablePreservesExistingCoprocessors() {
+    String key = CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY;
+    conf.setStrings(key, "existingCp");
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(true, conf, key);
+    List<String> result = Arrays.asList(conf.getStrings(key));
+    assertTrue(result.contains("existingCp"));
+    assertTrue(result.contains(MasterReadOnlyController.class.getName()));
+  }
+
+  @Test
+  public void 
testSyncReadOnlyConfigurationsReadOnlyDisableRemovesOnlyReadOnlyCoprocessor() {
+    Configuration conf = new Configuration(false);
+    String key = CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY;
+    String existingCp = "org.example.OtherCP";
+    conf.setStrings(key, existingCp, MasterReadOnlyController.class.getName());
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(false, conf, key);
+    String[] cps = conf.getStrings(key);
+    assertNotNull(cps);
+    assertEquals(1, cps.length);
+    assertEquals(existingCp, cps[0]);
+  }
+
+  @Test
+  public void testSyncReadOnlyConfigurationsIsIdempotent() {
+    Configuration conf = new Configuration(false);
+    String key = CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY;
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(true, conf, key);
+    CoprocessorConfigurationUtil.syncReadOnlyConfigurations(true, conf, key);
+    String[] cps = conf.getStrings(key);
+    assertNotNull(cps);
+    assertEquals(1, cps.length);
+  }
+}

Reply via email to