taklwu commented on code in PR #8044:
URL: https://github.com/apache/hbase/pull/8044#discussion_r3082492374
##########
hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java:
##########
@@ -1665,6 +1665,31 @@ public enum OperationStatusCode {
*/
public final static boolean REJECT_DECOMMISSIONED_HOSTS_DEFAULT = false;
+ /**
+ * Adds a suffix to the meta table name: value=’test’ -> ‘hbase:meta_test’
Added in HBASE-XXXXX to
Review Comment:
what should `HBASE-XXXXX` refer ? should we update this comment before
merging ?
##########
hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestCanStartHBaseInReadOnlyMode.java:
##########
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.*;
+import org.apache.hadoop.hbase.client.*;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.junit.*;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.TestName;
+
+@Category({ SecurityTests.class, LargeTests.class })
+@SuppressWarnings("deprecation")
+public class TestCanStartHBaseInReadOnlyMode {
+
+ @ClassRule
Review Comment:
nit: remove if we decide to use junit 5
##########
hbase-client/src/main/java/org/apache/hadoop/hbase/ActiveClusterSuffix.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.Objects;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.exceptions.DeserializationException;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import org.apache.hbase.thirdparty.com.google.common.base.Strings;
+
+import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
+import
org.apache.hadoop.hbase.shaded.protobuf.generated.ActiveClusterSuffixProtos;
+
+/**
+ * The read-replica cluster id for this cluster. It is serialized to the
filesystem and up into
+ * zookeeper. This is a container for the id. Also knows how to serialize and
deserialize the
+ * cluster id.
+ */
[email protected]
+public class ActiveClusterSuffix implements ClusterIdFile {
+ private final String cluster_id;
Review Comment:
nit: please follow the camel case in java class, update respectively in this
file and other file.
```suggestion
private final String clusterId;
```
##########
hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyController.java:
##########
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.Delete;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Row;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.master.HMaster;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestName;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Category({ SecurityTests.class, LargeTests.class })
+@SuppressWarnings("deprecation")
+public class TestReadOnlyController {
+
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestReadOnlyController.class);
+
+ private static final Logger LOG =
LoggerFactory.getLogger(TestReadOnlyController.class);
+ private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
+ private static final TableName TEST_TABLE =
TableName.valueOf("read_only_test_table");
+ private static final byte[] TEST_FAMILY =
Bytes.toBytes("read_only_table_col_fam");
+ private static HRegionServer hRegionServer;
+ private static HMaster hMaster;
+ private static Configuration conf;
+ private static Connection connection;
+ private static SingleProcessHBaseCluster cluster;
+
+ private static Table testTable;
+ @Rule
+ public TestName name = new TestName();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @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
+ // threshold
+ conf.setInt(HBASE_CLIENT_RETRIES_NUMBER, 1);
+ conf.setInt(HConstants.ZK_SESSION_TIMEOUT, 1000);
+
+ // Set up test class with Read-Only mode disabled so a table can be created
+ conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
+
+ try {
+ // Start the test cluster
+ cluster = TEST_UTIL.startMiniCluster(1);
+
+ hMaster = cluster.getMaster();
+ hRegionServer =
cluster.getRegionServerThreads().get(0).getRegionServer();
+ connection = ConnectionFactory.createConnection(conf);
+
+ // Create a test table
+ testTable = TEST_UTIL.createTable(TEST_TABLE, TEST_FAMILY);
+ } catch (Exception e) {
+ // Delete the created table, and clean up the connection and cluster
before throwing an
+ // exception
+ disableReadOnlyMode();
+ TEST_UTIL.deleteTable(TEST_TABLE);
+ connection.close();
+ TEST_UTIL.shutdownMiniCluster();
+ throw new RuntimeException(e);
+ }
+ }
+
+ @After
+ public void afterClass() throws Exception {
+ if (connection != null) {
+ connection.close();
+ }
+ TEST_UTIL.shutdownMiniCluster();
+ }
+
+ private static void enableReadOnlyMode() {
+ // Dynamically enable Read-Only mode if it is not active
+ if (!isReadOnlyModeEnabled()) {
Review Comment:
```suggestion
if (!ConfigurationUtil.isReadOnlyModeEnabled(conf)) {
```
##########
hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestRefreshHFilesFromClient.java:
##########
@@ -0,0 +1,139 @@
+/*
+ * 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.client;
+
+import static org.junit.Assert.assertTrue;
+
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.NamespaceNotFoundException;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.TableNotFoundException;
+import org.apache.hadoop.hbase.TestRefreshHFilesBase;
+import org.apache.hadoop.hbase.testclassification.ClientTests;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MediumTests.class, ClientTests.class })
+public class TestRefreshHFilesFromClient extends TestRefreshHFilesBase {
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestRefreshHFilesFromClient.class);
+
+ private static final TableName TEST_NONEXISTENT_TABLE =
+ TableName.valueOf("testRefreshHFilesNonExistentTable");
+ private static final String TEST_NONEXISTENT_NAMESPACE =
"testRefreshHFilesNonExistentNamespace";
+
+ @Before
+ public void setup() throws Exception {
+ baseSetup(false);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ baseTearDown();
+ }
+
+ @Test
+ public void testRefreshHFilesForTable() throws Exception {
+ try {
+ // Create table in default namespace
+ createTableAndWait(TEST_TABLE, TEST_FAMILY);
+
+ // RefreshHFiles for table
+ Long procId = admin.refreshHFiles(TEST_TABLE);
+ assertTrue(procId >= 0);
+ } catch (Exception e) {
+ Assert.fail("RefreshHFilesForTable Should Not Throw Exception: " + e);
+ throw new RuntimeException(e);
+ } finally {
+ // Delete table name post test execution
+ deleteTable(TEST_TABLE);
+ }
+ }
+
+ // Not creating table hence refresh should throw exception
+ @Test(expected = TableNotFoundException.class)
Review Comment:
nit: need some work for junit 4 to junit 5, but as mentioned, we at least
need to know if this is a must in master branch.
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/CompactSplit.java:
##########
@@ -442,6 +449,13 @@ private Optional<CompactionContext>
selectCompaction(HRegion region, HStore stor
LOG.info(String.format("User has disabled compactions"));
return Optional.empty();
}
+
+ // Should not allow compaction if cluster is in read-only mode
+ if (isReadOnlyEnabled()) {
+ LOG.info(String.format("Compaction request skipped as read-only mode is
on"));
Review Comment:
```suggestion
LOG.info("Compaction request skipped as read-only mode is on");
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/CompactSplit.java:
##########
@@ -344,6 +345,12 @@ protected void requestCompactionInternal(HRegion region,
HStore store, String wh
return;
}
+ // Should not allow compaction if cluster is in read-only mode
+ if (isReadOnlyEnabled()) {
+ LOG.info("Ignoring compaction request for " + region + ",because
read-only mode is on.");
Review Comment:
nit about log message
```suggestion
LOG.info("Ignoring compaction request for {} because read-only mode is
on.", region);
```
##########
hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestCanStartHBaseInReadOnlyMode.java:
##########
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.*;
+import org.apache.hadoop.hbase.client.*;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.junit.*;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.TestName;
+
+@Category({ SecurityTests.class, LargeTests.class })
+@SuppressWarnings("deprecation")
Review Comment:
nit: should we use tag of junit 5 before merging ? you may need to fix it
for other classes as well. but if we decide to fix it later, please file JIRA
and link it with https://issues.apache.org/jira/browse/HBASE-23671 ?
```suggestion
@Tag(SecurityTests.TAG)
@Tag(LargeTests.TAG)
```
##########
hbase-client/src/main/java/org/apache/hadoop/hbase/ActiveClusterSuffix.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.Objects;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.exceptions.DeserializationException;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.yetus.audience.InterfaceAudience;
+
+import org.apache.hbase.thirdparty.com.google.common.base.Strings;
+
+import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
+import
org.apache.hadoop.hbase.shaded.protobuf.generated.ActiveClusterSuffixProtos;
+
+/**
+ * The read-replica cluster id for this cluster. It is serialized to the
filesystem and up into
+ * zookeeper. This is a container for the id. Also knows how to serialize and
deserialize the
+ * cluster id.
+ */
[email protected]
+public class ActiveClusterSuffix implements ClusterIdFile {
+ private final String cluster_id;
+ private final String suffix;
+
+ public static class Parser implements
ClusterIdFileParser<ActiveClusterSuffix> {
+
+ @Override
+ public String getFileName() {
+ return HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME;
+ }
+
+ /**
+ * Parse the serialized representation of the {@link ActiveClusterSuffix}
+ * @param bytes A pb serialized {@link ActiveClusterSuffix} instance with
pb magic prefix
+ * @return An instance of {@link ActiveClusterSuffix} made from
<code>bytes</code>
+ * @see #toByteArray()
+ */
+ @Override
+ public ActiveClusterSuffix parseFrom(byte[] bytes) throws
DeserializationException {
+ if (ProtobufUtil.isPBMagicPrefix(bytes)) {
+ int pblen = ProtobufUtil.lengthOfPBMagic();
+ ActiveClusterSuffixProtos.ActiveClusterSuffix.Builder builder =
+ ActiveClusterSuffixProtos.ActiveClusterSuffix.newBuilder();
+ ActiveClusterSuffixProtos.ActiveClusterSuffix cs = null;
+ try {
+ ProtobufUtil.mergeFrom(builder, bytes, pblen, bytes.length - pblen);
+ cs = builder.build();
+ } catch (IOException e) {
+ throw new DeserializationException(e);
+ }
+ return convert(cs);
+ } else {
+ // Presume it was written out this way, the old way.
+ return new ActiveClusterSuffix(Bytes.toString(bytes));
+ }
+ }
+
+ @Override
+ public ActiveClusterSuffix readString(String input) {
+ return new ActiveClusterSuffix(input);
+ }
+ }
+
+ public ActiveClusterSuffix(final String ci, final String suffix) {
+ this.cluster_id = ci;
Review Comment:
```suggestion
public ActiveClusterSuffix(final String clusterId, final String suffix) {
this.clusterId = clusterId;
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java:
##########
@@ -382,4 +392,55 @@ public void stop() {
public void logFileSystemState(Logger log) throws IOException {
CommonFSUtils.logFileSystemState(fs, rootdir, log);
}
+
+ private void negotiateActiveClusterSuffixFile(long wait) throws IOException {
Review Comment:
nit: do we have any JIRA for documenting the switch to an active cluster
from a read-replica by using the `hbase.meta.table.suffix`?
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java:
##########
@@ -382,4 +392,55 @@ public void stop() {
public void logFileSystemState(Logger log) throws IOException {
CommonFSUtils.logFileSystemState(fs, rootdir, log);
}
+
+ private void negotiateActiveClusterSuffixFile(long wait) throws IOException {
+ this.activeClusterSuffix = ActiveClusterSuffix.fromConfig(conf,
getClusterId());
+ if (!isReadOnlyModeEnabled(conf)) {
+ try {
+ // verify the contents against the config set
+ ActiveClusterSuffix acs =
+ FSUtils.getClusterIdFile(fs, rootdir, new
ActiveClusterSuffix.Parser());
+ if (acs == null) {
+ throw new FileNotFoundException("[Read-replica feature] Active
Cluster Suffix File "
+ + new Path(rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME) +
" not found");
+ }
+ LOG.debug(
+ "Negotiating active cluster suffix file. File {} : File Suffix {} :
Configured suffix {} : Cluster ID : {}",
+ new Path(rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME), acs,
activeClusterSuffix,
+ getClusterId());
+ // Suffix file exists and we're in read/write mode. Content should
match.
+ if (!this.activeClusterSuffix.equals(acs)) {
+ // throw error
+ LOG.info(
Review Comment:
nit: should the mismatch message be error level? or should it be at least a
warning?
```suggestion
LOG.error(
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AbstractReadOnlyController.java:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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 java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Set;
+import org.apache.hadoop.fs.FSDataInputStream;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.ActiveClusterSuffix;
+import org.apache.hadoop.hbase.Coprocessor;
+import org.apache.hadoop.hbase.CoprocessorEnvironment;
+import org.apache.hadoop.hbase.HBaseInterfaceAudience;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.WriteAttemptedOnReadOnlyClusterException;
+import org.apache.hadoop.hbase.coprocessor.ObserverContext;
+import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
+import org.apache.hadoop.hbase.exceptions.DeserializationException;
+import org.apache.hadoop.hbase.master.MasterFileSystem;
+import org.apache.hadoop.hbase.master.region.MasterRegionFactory;
+import org.apache.hadoop.hbase.util.FSUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
[email protected](HBaseInterfaceAudience.CONFIG)
+public abstract class AbstractReadOnlyController implements Coprocessor {
+ private static final Logger LOG =
LoggerFactory.getLogger(AbstractReadOnlyController.class);
+
+ private static final Set<TableName> writableTables =
+ Set.of(TableName.META_TABLE_NAME, MasterRegionFactory.TABLE_NAME);
+
+ public static boolean
+ isWritableInReadOnlyMode(final ObserverContext<? extends
RegionCoprocessorEnvironment> c) {
+ return
writableTables.contains(c.getEnvironment().getRegionInfo().getTable());
+ }
+
+ public static boolean isWritableInReadOnlyMode(final TableName tableName) {
+ return writableTables.contains(tableName);
+ }
+
+ protected void internalReadOnlyGuard() throws
WriteAttemptedOnReadOnlyClusterException {
+ throw new WriteAttemptedOnReadOnlyClusterException("Operation not allowed
in Read-Only Mode");
+ }
+
+ @Override
+ public void start(CoprocessorEnvironment env) throws IOException {
+ }
+
+ @Override
+ public void stop(CoprocessorEnvironment env) {
+ }
+
+ 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);
+
+ try {
+ if (readOnlyEnabled) {
+ // ENABLING READ-ONLY (false -> true), delete the active cluster file.
Review Comment:
nit: would below fast return better ?
```suggestion
if (!readOnlyEnabled) {
return;
}
FileSystem fs = mfs.getFileSystem();
Path rootDir = mfs.getRootDir();
Path activeClusterFile = new Path(rootDir,
HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME);
try {
// ENABLING READ-ONLY (false -> true), delete the active cluster
file.
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java:
##########
@@ -382,4 +392,55 @@ public void stop() {
public void logFileSystemState(Logger log) throws IOException {
CommonFSUtils.logFileSystemState(fs, rootdir, log);
}
+
+ private void negotiateActiveClusterSuffixFile(long wait) throws IOException {
+ this.activeClusterSuffix = ActiveClusterSuffix.fromConfig(conf,
getClusterId());
+ if (!isReadOnlyModeEnabled(conf)) {
Review Comment:
```suggestion
if (!ConfigurationUtil.isReadOnlyModeEnabled(conf)) {
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/CompactSplit.java:
##########
@@ -857,6 +871,11 @@ public boolean isCompactionsEnabled() {
return compactionsEnabled;
}
+ private boolean isReadOnlyEnabled() {
+ return conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
+ HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
+ }
+
Review Comment:
```suggestion
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/master/HMaster.java:
##########
@@ -4483,13 +4505,17 @@ public void onConfigurationChange(Configuration
newConf) {
}
// append the quotas observer back to the master coprocessor key
setQuotasObserver(newConf);
- // update region server coprocessor if the configuration has changed.
- if (
- CoprocessorConfigurationUtil.checkConfigurationChange(this.cpHost,
newConf,
- CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY) && !maintenanceMode
- ) {
- LOG.info("Update the master coprocessor(s) because the configuration has
changed");
- this.cpHost = new MasterCoprocessorHost(this, newConf);
+
+ boolean originalIsReadOnlyEnabled = this.isGlobalReadOnlyEnabled;
+
+ CoprocessorConfigurationUtil.maybeUpdateCoprocessors(newConf,
this.isGlobalReadOnlyEnabled,
+ this.cpHost, CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
this.maintenanceMode,
+ this.toString(), val -> this.isGlobalReadOnlyEnabled = val,
+ conf -> initializeCoprocessorHost(newConf));
+
+ if (this.isGlobalReadOnlyEnabled != originalIsReadOnlyEnabled) {
Review Comment:
nit: if we don't need to manage the active cluster Id for the non-readonly
cluster, maybe we should skip early.
```suggestion
if (this.isGlobalReadOnlyEnabled != originalIsReadOnlyEnabled &&
this.isGlobalReadOnlyEnabled) {
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/util/CoprocessorConfigurationUtil.java:
##########
@@ -102,4 +116,129 @@ private static boolean
hasCoprocessorsConfigured(Configuration conf, String... c
}
return false;
}
+
+ 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);
+ };
+ }
+
+ /**
+ * This method adds or removes relevant ReadOnlyController coprocessors to
the provided
+ * configuration based on whether read-only mode is enabled.
+ * @param conf The up-to-date configuration used to determine
how to handle
+ * coprocessors
+ * @param coprocessorConfKey The configuration key name
+ */
+ public static void syncReadOnlyConfigurations(Configuration conf, String
coprocessorConfKey) {
+ boolean isReadOnlyModeEnabled =
conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
+ HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
Review Comment:
```suggestion
boolean isReadOnlyModeEnabled =
ConfigurationUtil.isReadOnlyModeEnabled(conf);
```
##########
hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java:
##########
@@ -382,4 +392,55 @@ public void stop() {
public void logFileSystemState(Logger log) throws IOException {
CommonFSUtils.logFileSystemState(fs, rootdir, log);
}
+
+ private void negotiateActiveClusterSuffixFile(long wait) throws IOException {
+ this.activeClusterSuffix = ActiveClusterSuffix.fromConfig(conf,
getClusterId());
+ if (!isReadOnlyModeEnabled(conf)) {
+ try {
+ // verify the contents against the config set
+ ActiveClusterSuffix acs =
+ FSUtils.getClusterIdFile(fs, rootdir, new
ActiveClusterSuffix.Parser());
+ if (acs == null) {
+ throw new FileNotFoundException("[Read-replica feature] Active
Cluster Suffix File "
+ + new Path(rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME) +
" not found");
+ }
+ LOG.debug(
+ "Negotiating active cluster suffix file. File {} : File Suffix {} :
Configured suffix {} : Cluster ID : {}",
+ new Path(rootdir, HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME), acs,
activeClusterSuffix,
+ getClusterId());
+ // Suffix file exists and we're in read/write mode. Content should
match.
+ if (!this.activeClusterSuffix.equals(acs)) {
+ // throw error
+ LOG.info(
+ "[Read-replica feature] Another cluster is running in active
(read-write) mode on this "
+ + "storage location. Active cluster ID: {}, This cluster ID {}.
Rootdir location {} ",
+ acs, activeClusterSuffix, rootdir);
+ throw new IOException("Cannot start master, because another cluster
is running in active "
+ + "(read-write) mode on this storage location. Active Cluster Id:
" + acs
+ + ", This cluster Id: " + activeClusterSuffix);
+ }
+ LOG.info(
+ "[Read-replica feature] This is the active cluster on this storage
location with cluster id: {}",
+ activeClusterSuffix);
+ } catch (FileNotFoundException fnfe) {
+ // We're in read/write mode, but suffix file missing, let's create it
+ FSUtils.setClusterIdFile(fs, rootdir,
HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME,
+ activeClusterSuffix, wait);
+ LOG.info("[Read-replica feature] Created Active cluster suffix file:
{}, with content: {}",
+ HConstants.ACTIVE_CLUSTER_SUFFIX_FILE_NAME, activeClusterSuffix);
+ }
+ } else {
+ // This is a read-only cluster, don't care about suffix file
+ LOG.info("[Read-replica feature] Replica cluster is being started in
Read Only Mode");
+ }
+ }
+
+ public ActiveClusterSuffix getActiveClusterSuffix() {
+ return activeClusterSuffix;
+ }
+
+ private boolean isReadOnlyModeEnabled(Configuration conf) {
+ return conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
+ HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
+ }
Review Comment:
```suggestion
```
##########
hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyController.java:
##########
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.Delete;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Row;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.master.HMaster;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestName;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Category({ SecurityTests.class, LargeTests.class })
+@SuppressWarnings("deprecation")
+public class TestReadOnlyController {
+
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestReadOnlyController.class);
+
+ private static final Logger LOG =
LoggerFactory.getLogger(TestReadOnlyController.class);
+ private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
+ private static final TableName TEST_TABLE =
TableName.valueOf("read_only_test_table");
+ private static final byte[] TEST_FAMILY =
Bytes.toBytes("read_only_table_col_fam");
+ private static HRegionServer hRegionServer;
+ private static HMaster hMaster;
+ private static Configuration conf;
+ private static Connection connection;
+ private static SingleProcessHBaseCluster cluster;
+
+ private static Table testTable;
+ @Rule
+ public TestName name = new TestName();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @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
+ // threshold
+ conf.setInt(HBASE_CLIENT_RETRIES_NUMBER, 1);
+ conf.setInt(HConstants.ZK_SESSION_TIMEOUT, 1000);
+
+ // Set up test class with Read-Only mode disabled so a table can be created
+ conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
+
+ try {
+ // Start the test cluster
+ cluster = TEST_UTIL.startMiniCluster(1);
+
+ hMaster = cluster.getMaster();
+ hRegionServer =
cluster.getRegionServerThreads().get(0).getRegionServer();
+ connection = ConnectionFactory.createConnection(conf);
+
+ // Create a test table
+ testTable = TEST_UTIL.createTable(TEST_TABLE, TEST_FAMILY);
+ } catch (Exception e) {
+ // Delete the created table, and clean up the connection and cluster
before throwing an
+ // exception
+ disableReadOnlyMode();
+ TEST_UTIL.deleteTable(TEST_TABLE);
+ connection.close();
+ TEST_UTIL.shutdownMiniCluster();
+ throw new RuntimeException(e);
+ }
+ }
+
+ @After
+ public void afterClass() throws Exception {
+ if (connection != null) {
+ connection.close();
+ }
+ TEST_UTIL.shutdownMiniCluster();
+ }
+
+ private static void enableReadOnlyMode() {
+ // Dynamically enable Read-Only mode if it is not active
+ if (!isReadOnlyModeEnabled()) {
+ LOG.info("Dynamically enabling Read-Only mode by setting {} to true",
+ HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
+ conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+ notifyObservers();
+ }
+ }
+
+ private static void disableReadOnlyMode() {
+ // Dynamically disable Read-Only mode if it is active
+ if (isReadOnlyModeEnabled()) {
Review Comment:
```suggestion
if (ConfigurationUtil.isReadOnlyModeEnabled(conf)) {
```
##########
hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestReadOnlyController.java:
##########
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.access;
+
+import static org.apache.hadoop.hbase.HConstants.HBASE_CLIENT_RETRIES_NUMBER;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseTestingUtil;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.SingleProcessHBaseCluster;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.Delete;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Row;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.master.HMaster;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.testclassification.SecurityTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestName;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Category({ SecurityTests.class, LargeTests.class })
+@SuppressWarnings("deprecation")
+public class TestReadOnlyController {
+
+ @ClassRule
+ public static final HBaseClassTestRule CLASS_RULE =
+ HBaseClassTestRule.forClass(TestReadOnlyController.class);
+
+ private static final Logger LOG =
LoggerFactory.getLogger(TestReadOnlyController.class);
+ private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
+ private static final TableName TEST_TABLE =
TableName.valueOf("read_only_test_table");
+ private static final byte[] TEST_FAMILY =
Bytes.toBytes("read_only_table_col_fam");
+ private static HRegionServer hRegionServer;
+ private static HMaster hMaster;
+ private static Configuration conf;
+ private static Connection connection;
+ private static SingleProcessHBaseCluster cluster;
+
+ private static Table testTable;
+ @Rule
+ public TestName name = new TestName();
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @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
+ // threshold
+ conf.setInt(HBASE_CLIENT_RETRIES_NUMBER, 1);
+ conf.setInt(HConstants.ZK_SESSION_TIMEOUT, 1000);
+
+ // Set up test class with Read-Only mode disabled so a table can be created
+ conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
+
+ try {
+ // Start the test cluster
+ cluster = TEST_UTIL.startMiniCluster(1);
+
+ hMaster = cluster.getMaster();
+ hRegionServer =
cluster.getRegionServerThreads().get(0).getRegionServer();
+ connection = ConnectionFactory.createConnection(conf);
+
+ // Create a test table
+ testTable = TEST_UTIL.createTable(TEST_TABLE, TEST_FAMILY);
+ } catch (Exception e) {
+ // Delete the created table, and clean up the connection and cluster
before throwing an
+ // exception
+ disableReadOnlyMode();
+ TEST_UTIL.deleteTable(TEST_TABLE);
+ connection.close();
+ TEST_UTIL.shutdownMiniCluster();
+ throw new RuntimeException(e);
+ }
+ }
+
+ @After
+ public void afterClass() throws Exception {
+ if (connection != null) {
+ connection.close();
+ }
+ TEST_UTIL.shutdownMiniCluster();
+ }
+
+ private static void enableReadOnlyMode() {
+ // Dynamically enable Read-Only mode if it is not active
+ if (!isReadOnlyModeEnabled()) {
+ LOG.info("Dynamically enabling Read-Only mode by setting {} to true",
+ HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
+ conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, true);
+ notifyObservers();
+ }
+ }
+
+ private static void disableReadOnlyMode() {
+ // Dynamically disable Read-Only mode if it is active
+ if (isReadOnlyModeEnabled()) {
+ LOG.info("Dynamically disabling Read-Only mode by setting {} to false",
+ HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
+ conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, false);
+ notifyObservers();
+ }
+ }
+
+ private static boolean isReadOnlyModeEnabled() {
+ return conf.getBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY,
+ HConstants.HBASE_GLOBAL_READONLY_ENABLED_DEFAULT);
+ }
Review Comment:
```suggestion
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]