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 bf03a377232 HBASE-29959 Cluster started in read-only mode mistakenly 
deletes suffix file during startup (#7881)
bf03a377232 is described below

commit bf03a3772329cc46c202e2bcd36532212e84cbd2
Author: Anuj Sharma <[email protected]>
AuthorDate: Thu Mar 12 00:53:35 2026 +0530

    HBASE-29959 Cluster started in read-only mode mistakenly deletes suffix 
file during startup (#7881)
    
    * HBASE-29959 Cluster started in read-only mode mistakenly deletes suffix 
file during startup
    
    * Move log message to if block.
    
    * Close file input stream
    
    * Change the getter which does not mutate the suffix data
---
 .../hadoop/hbase/master/MasterFileSystem.java      |   5 +-
 .../access/AbstractReadOnlyController.java         |  26 +++-
 .../security/access/TestReadOnlyController.java    |  27 ----
 .../TestReadOnlyManageActiveClusterFile.java       | 153 +++++++++++++++++++++
 4 files changed, 174 insertions(+), 37 deletions(-)

diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java
index 034faa05802..1734f03a273 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java
@@ -415,7 +415,7 @@ public class MasterFileSystem {
           acs, getActiveClusterSuffix());
       } catch (FileNotFoundException fnfe) {
         // this is the active cluster, create active cluster suffix file if it 
does not exist
-        FSUtils.setActiveClusterSuffix(fs, rootdir, 
getSuffixFileDataToWrite(), wait);
+        FSUtils.setActiveClusterSuffix(fs, rootdir, 
computeAndSetSuffixFileDataToWrite(), wait);
       }
     } else {
       // this is a replica cluster
@@ -447,8 +447,7 @@ public class MasterFileSystem {
     return str.getBytes(StandardCharsets.UTF_8);
   }
 
-  //
-  public byte[] getSuffixFileDataToWrite() {
+  public byte[] computeAndSetSuffixFileDataToWrite() {
     String str = getClusterId().toString() + ":" + 
getActiveClusterSuffixFromConfig(conf);
     this.activeClusterSuffix = new ActiveClusterSuffix(str);
     return str.getBytes(StandardCharsets.UTF_8);
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 d5039f84348..c8c1837b21b 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
@@ -17,7 +17,11 @@
  */
 package org.apache.hadoop.hbase.security.access;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.apache.commons.io.IOUtils;
+import org.apache.hadoop.fs.FSDataInputStream;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.Coprocessor;
@@ -59,25 +63,33 @@ public abstract class AbstractReadOnlyController implements 
Coprocessor {
         // ENABLING READ-ONLY (false -> true), delete the active cluster file.
         LOG.debug("Global read-only mode is being ENABLED. Deleting active 
cluster file: {}",
           activeClusterFile);
-        try {
-          if (fs.exists(activeClusterFile)) {
+        try (FSDataInputStream in = fs.open(activeClusterFile)) {
+          String actualClusterFileData = IOUtils.toString(in, 
StandardCharsets.UTF_8);
+          String expectedClusterFileData = mfs.getSuffixFromConfig();
+          if (actualClusterFileData.equals(expectedClusterFileData)) {
             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);
+            LOG.debug(
+              "Active cluster file data does not match expected data. "
+                + "Not deleting the file to avoid potential inconsistency. "
+                + "Actual data: {}, Expected data: {}",
+              new String(actualClusterFileData), new 
String(expectedClusterFileData));
           }
-        } catch (IOException e) {
+      } catch (FileNotFoundException e) {
+          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: {}. "
               + "Read-only flag will be updated, but file system state is 
inconsistent.",
-            activeClusterFile);
+            activeClusterFile, e);
         }
       } else {
         // DISABLING READ-ONLY (true -> false), create the active cluster file 
id file
         int wait = 
mfs.getConfiguration().getInt(HConstants.THREAD_WAKE_FREQUENCY, 10 * 1000);
         if (!fs.exists(activeClusterFile)) {
-          FSUtils.setActiveClusterSuffix(fs, rootDir, 
mfs.getSuffixFileDataToWrite(), wait);
+          FSUtils.setActiveClusterSuffix(fs, rootDir, 
mfs.computeAndSetSuffixFileDataToWrite(), wait);
         } else {
           LOG.debug("Active cluster file already exists at: {}. No need to 
create it again.",
             activeClusterFile);
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 15ee2fdd45b..31e5f8193ba 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
@@ -18,15 +18,11 @@
 package org.apache.hadoop.hbase.security.access;
 
 import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.FileSystem;
-import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hbase.HBaseClassTestRule;
 import org.apache.hadoop.hbase.HBaseTestingUtil;
 import org.apache.hadoop.hbase.HConstants;
@@ -39,7 +35,6 @@ 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.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;
@@ -152,14 +147,6 @@ public class TestReadOnlyController {
     hRegionServer.getConfigurationManager().notifyAllObservers(conf);
   }
 
-  private static boolean isActiveClusterIdFileExists() throws IOException {
-    MasterFileSystem mfs = hMaster.getMasterFileSystem();
-    Path rootDir = mfs.getRootDir();
-    FileSystem fs = mfs.getFileSystem();
-    Path activeClusterFile = new Path(rootDir, 
HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME);
-    return fs.exists(activeClusterFile);
-  }
-
   // The test case for successfully creating a table with Read-Only mode 
disabled happens when
   // setting up the test class, so we only need a test function for a failed 
table creation.
   @Test
@@ -227,18 +214,4 @@ public class TestReadOnlyController {
     // This should throw the IOException
     testTable.batch(actions, null);
   }
-
-  @Test
-  public void testActiveClusterIdFileCreationWhenReadOnlyDisabled()
-    throws IOException, InterruptedException {
-    disableReadOnlyMode();
-    assertTrue(isActiveClusterIdFileExists());
-  }
-
-  @Test
-  public void testActiveClusterIdFileDeletionWhenReadOnlyEnabled()
-    throws IOException, InterruptedException {
-    enableReadOnlyMode();
-    assertFalse(isActiveClusterIdFileExists());
-  }
 }
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyManageActiveClusterFile.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyManageActiveClusterFile.java
new file mode 100644
index 00000000000..eaeda593ea4
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyManageActiveClusterFile.java
@@ -0,0 +1,153 @@
+/*
+ * 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.assertTrue;
+
+import java.io.IOException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FSDataOutputStream;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.HConstants;
+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.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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Category({ SecurityTests.class, MediumTests.class })
+public class TestReadOnlyManageActiveClusterFile {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestReadOnlyManageActiveClusterFile.class);
+
+  private static final Logger LOG =
+    LoggerFactory.getLogger(TestReadOnlyManageActiveClusterFile.class);
+  private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
+  private static final int NUM_RS = 1;
+  private static Configuration conf;
+  HMaster master;
+  HRegionServer regionServer;
+
+  MasterFileSystem mfs;
+  Path rootDir;
+  FileSystem fs;
+  Path activeClusterFile;
+
+  @Before
+  public void setup() throws Exception {
+    conf = TEST_UTIL.getConfiguration();
+
+    // Set up test class with Read-Only mode disabled so a table can be created
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
+    // Start the test cluster
+    TEST_UTIL.startMiniCluster(NUM_RS);
+    master = TEST_UTIL.getMiniHBaseCluster().getMaster();
+    regionServer = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
+
+    mfs = master.getMasterFileSystem();
+    rootDir = mfs.getRootDir();
+    fs = mfs.getFileSystem();
+    activeClusterFile = new Path(rootDir, 
HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    TEST_UTIL.shutdownMiniCluster();
+  }
+
+  private void setReadOnlyMode(boolean enabled) {
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, enabled);
+    master.getConfigurationManager().notifyAllObservers(conf);
+    regionServer.getConfigurationManager().notifyAllObservers(conf);
+  }
+
+  private void restartCluster() throws IOException, InterruptedException {
+    TEST_UTIL.getMiniHBaseCluster().shutdown();
+    TEST_UTIL.restartHBaseCluster(NUM_RS);
+    TEST_UTIL.waitUntilNoRegionsInTransition();
+
+    master = TEST_UTIL.getMiniHBaseCluster().getMaster();
+    regionServer = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0);
+
+    MasterFileSystem mfs = master.getMasterFileSystem();
+    fs = mfs.getFileSystem();
+    activeClusterFile = new Path(mfs.getRootDir(), 
HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME);
+  }
+
+  private void overwriteExistingFile() throws IOException {
+    try (FSDataOutputStream out = fs.create(activeClusterFile, true)) {
+      out.writeBytes("newClusterId");
+    }
+  }
+
+  private boolean activeClusterIdFileExists() throws IOException {
+    return fs.exists(activeClusterFile);
+  }
+
+  @Test
+  public void testActiveClusterIdFileCreationWhenReadOnlyDisabled()
+    throws IOException, InterruptedException {
+    setReadOnlyMode(false);
+    assertTrue(activeClusterIdFileExists());
+  }
+
+  @Test
+  public void testActiveClusterIdFileDeletionWhenReadOnlyEnabled()
+    throws IOException, InterruptedException {
+    setReadOnlyMode(true);
+    assertFalse(activeClusterIdFileExists());
+  }
+
+  @Test
+  public void 
testDeleteActiveClusterIdFileWhenSwitchingToReadOnlyIfOwnedByCluster()
+    throws IOException, InterruptedException {
+    // At the start cluster is in active mode hence set readonly mode and 
restart
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+    // Restart the cluster to trigger the deletion of the active cluster ID 
file
+    restartCluster();
+    // Should delete the active cluster ID file since it is owned by the 
cluster
+    assertFalse(activeClusterIdFileExists());
+  }
+
+  @Test
+  public void 
testDoNotDeleteActiveClusterIdFileWhenSwitchingToReadOnlyIfNotOwnedByCluster()
+    throws IOException, InterruptedException {
+    // Change the content of Active Cluster file to simulate the scenario 
where the file is not
+    // owned by the cluster and then set readonly mode and restart
+    overwriteExistingFile();
+    // At the start cluster is in active mode hence set readonly mode and 
restart
+    conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+    // Restart the cluster to trigger the deletion of the active cluster ID 
file
+    restartCluster();
+    // As Active cluster file is not owned by the cluster, it should not be 
deleted even when
+    // switching to readonly mode
+    assertTrue(activeClusterIdFileExists());
+  }
+}

Reply via email to