diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java index 6df721b..1b4d154 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java @@ -43,7 +43,6 @@ import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; @@ -77,6 +76,7 @@ import org.apache.hadoop.hbase.master.handler.EnableTableHandler; import org.apache.hadoop.hbase.master.handler.OpenedRegionHandler; import org.apache.hadoop.hbase.protobuf.generated.RegionServerStatusProtos.RegionStateTransition; import org.apache.hadoop.hbase.protobuf.generated.RegionServerStatusProtos.RegionStateTransition.TransitionCode; +import org.apache.hadoop.hbase.quotas.RegionStateListener; import org.apache.hadoop.hbase.regionserver.RegionAlreadyInTransitionException; import org.apache.hadoop.hbase.regionserver.RegionMergeTransaction; import org.apache.hadoop.hbase.regionserver.RegionOpeningState; @@ -98,6 +98,7 @@ import org.apache.hadoop.hbase.zookeeper.ZKTable; import org.apache.hadoop.hbase.zookeeper.ZKUtil; import org.apache.hadoop.hbase.zookeeper.ZooKeeperListener; import org.apache.hadoop.ipc.RemoteException; +import org.apache.hadoop.util.StringUtils; import org.apache.zookeeper.AsyncCallback; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.KeeperException.NoNodeException; @@ -107,6 +108,7 @@ import org.apache.zookeeper.data.Stat; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.LinkedHashMultimap; +import org.apache.hadoop.hbase.classification.InterfaceAudience; /** * Manages and performs region assignment. @@ -265,6 +267,8 @@ public class AssignmentManager extends ZooKeeperListener { /** Listeners that are called on assignment events. */ private List listeners = new CopyOnWriteArrayList(); + private RegionStateListener regionStateListener; + /** * Constructs a new assignment manager. * @@ -4195,17 +4199,34 @@ public class AssignmentManager extends ZooKeeperListener { break; case READY_TO_SPLIT: + try { + regionStateListener.onRegionSplit(hri); + } catch (IOException exp) { + errorMsg = StringUtils.stringifyException(exp); + break; + } case SPLIT_PONR: case SPLIT: case SPLIT_REVERTED: - errorMsg = onRegionSplit(serverName, code, hri, - HRegionInfo.convert(transition.getRegionInfo(1)), - HRegionInfo.convert(transition.getRegionInfo(2))); + errorMsg = + onRegionSplit(serverName, code, hri, HRegionInfo.convert(transition.getRegionInfo(1)), + HRegionInfo.convert(transition.getRegionInfo(2))); + if (code == TransitionCode.SPLIT_REVERTED) { + try { + regionStateListener.onRegionSplitReverted(hri); + } catch (IOException exp) { + LOG.warn(StringUtils.stringifyException(exp)); + } + } break; - case READY_TO_MERGE: case MERGE_PONR: case MERGED: + try { + regionStateListener.onRegionMerged(hri); + } catch (IOException exp) { + errorMsg = StringUtils.stringifyException(exp); + } case MERGE_REVERTED: errorMsg = onRegionMerge(serverName, code, hri, HRegionInfo.convert(transition.getRegionInfo(1)), @@ -4228,4 +4249,8 @@ public class AssignmentManager extends ZooKeeperListener { public LoadBalancer getBalancer() { return this.balancer; } + + void setRegionStateListener(RegionStateListener listener) { + this.regionStateListener = listener; + } } 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 ca6a994..8f9160b 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 @@ -108,6 +108,7 @@ import org.apache.hadoop.hbase.master.snapshot.SnapshotManager; import org.apache.hadoop.hbase.monitoring.MemoryBoundedLogMessageBuffer; import org.apache.hadoop.hbase.monitoring.MonitoredTask; import org.apache.hadoop.hbase.monitoring.TaskMonitor; +import org.apache.hadoop.hbase.namespace.NamespaceAuditor; import org.apache.hadoop.hbase.procedure.MasterProcedureManager; import org.apache.hadoop.hbase.procedure.MasterProcedureManagerHost; import org.apache.hadoop.hbase.protobuf.ProtobufUtil; @@ -215,6 +216,8 @@ import org.apache.hadoop.hbase.protobuf.generated.RegionServerStatusProtos.Repor import org.apache.hadoop.hbase.protobuf.generated.RegionServerStatusProtos.ReportRegionStateTransitionRequest; import org.apache.hadoop.hbase.protobuf.generated.RegionServerStatusProtos.ReportRegionStateTransitionResponse; import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos.SplitLogTask.RecoveryMode; +import org.apache.hadoop.hbase.quotas.MasterQuotaManager; +import org.apache.hadoop.hbase.quotas.RegionStateListener; import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost; import org.apache.hadoop.hbase.regionserver.RegionSplitPolicy; import org.apache.hadoop.hbase.replication.regionserver.Replication; @@ -463,6 +466,8 @@ MasterServices, Server { /** The following is used in master recovery scenario to re-register listeners */ private List registeredZKListenersBeforeRecovery; + private MasterQuotaManager quotaManager; + /** * Initializes the HMaster. The steps are as follows: *

@@ -1007,6 +1012,8 @@ MasterServices, Server { status.setStatus("Starting namespace manager"); initNamespace(); + status.setStatus("Starting namespace auditor"); + initQuotaManager(); if (this.cpHost != null) { try { @@ -1165,6 +1172,12 @@ MasterServices, Server { tableNamespaceManager.start(); } + private void initQuotaManager() throws IOException { + quotaManager = new MasterQuotaManager(this); + this.assignmentManager.setRegionStateListener((RegionStateListener) quotaManager); + quotaManager.start(); + } + private void splitMetaLogBeforeAssignment(ServerName currentMetaServer) throws IOException { if (RecoveryMode.LOG_REPLAY == this.getMasterFileSystem().getLogRecoveryMode()) { // In log replay mode, we mark hbase:meta region as recovering in ZK @@ -1847,6 +1860,7 @@ MasterServices, Server { HRegionInfo[] newRegions = getHRegionInfos(hTableDescriptor, splitKeys); checkInitialized(); sanityCheckTableDescriptor(hTableDescriptor); + this.quotaManager.checkNamespaceTableAndRegionQuota(hTableDescriptor.getTableName(), newRegions.length); if (cpHost != null) { cpHost.preCreateTable(hTableDescriptor, newRegions); } @@ -3567,4 +3581,9 @@ MasterServices, Server { .getDefaultLoadBalancerClass().getName()); } + @Override + public MasterQuotaManager getMasterQuotaManager() { + return this.quotaManager; + } + } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java index c402758..bd8b378 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java @@ -32,6 +32,7 @@ import org.apache.hadoop.hbase.TableDescriptors; import org.apache.hadoop.hbase.TableNotDisabledException; import org.apache.hadoop.hbase.TableNotFoundException; import org.apache.hadoop.hbase.executor.ExecutorService; +import org.apache.hadoop.hbase.quotas.MasterQuotaManager; import com.google.protobuf.Service; @@ -250,4 +251,9 @@ public interface MasterServices extends Server { * @throws IOException */ public List listTableNamesByNamespace(String name) throws IOException; + + /** + * @return Master's instance of {@link MasterQuotaManager} + */ + MasterQuotaManager getMasterQuotaManager(); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/TableNamespaceManager.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/TableNamespaceManager.java index 99d4fb3..e5ab3c7 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/TableNamespaceManager.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/TableNamespaceManager.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.util.NavigableSet; +import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.hbase.classification.InterfaceAudience; @@ -30,6 +31,7 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.hbase.CellUtil; +import org.apache.hadoop.hbase.DoNotRetryIOException; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.HTableDescriptor; @@ -71,6 +73,9 @@ public class TableNamespaceManager { private ZKNamespaceManager zkNamespaceManager; private boolean initialized; + public static final String KEY_MAX_REGIONS = "hbase.namespace.quota.maxregions"; + public static final String KEY_MAX_TABLES = "hbase.namespace.quota.maxtables"; + static final String NS_INIT_TIMEOUT = "hbase.master.namespace.init.timeout"; static final int DEFAULT_NS_INIT_TIMEOUT = 60000; @@ -148,10 +153,14 @@ public class TableNamespaceManager { if (get(table, ns.getName()) != null) { throw new NamespaceExistException(ns.getName()); } + validateTableAndRegionCount(ns); FileSystem fs = masterServices.getMasterFileSystem().getFileSystem(); fs.mkdirs(FSUtils.getNamespaceDir( masterServices.getMasterFileSystem().getRootDir(), ns.getName())); upsert(table, ns); + if (this.masterServices.isInitialized()) { + this.masterServices.getMasterQuotaManager().setNamespaceQuota(ns); + } } private void upsert(HTable table, NamespaceDescriptor ns) throws IOException { @@ -203,6 +212,7 @@ public class TableNamespaceManager { masterServices.getMasterFileSystem().getRootDir(), name), true)) { throw new IOException("Failed to remove namespace: "+name); } + this.masterServices.getMasterQuotaManager().removeNamespaceQuota(name); } public synchronized NavigableSet list() throws IOException { @@ -301,4 +311,47 @@ public class TableNamespaceManager { return !masterServices.getAssignmentManager() .getRegionStates().getRegionsOfTable(TableName.NAMESPACE_TABLE_NAME).isEmpty(); } + + void validateTableAndRegionCount(NamespaceDescriptor desc) throws IOException { + if (getMaxRegions(desc) <= 0) { + throw new ConstraintException("The max region quota for " + desc.getName() + + " is less than or equal to zero."); + } + if (getMaxTables(desc) <= 0) { + throw new ConstraintException("The max tables quota for " + desc.getName() + + " is less than or equal to zero."); + } + } + + public static long getMaxTables(NamespaceDescriptor ns) throws IOException { + String value = ns.getConfigurationValue(KEY_MAX_TABLES); + long maxTables = 0; + if (StringUtils.isNotEmpty(value)) { + try { + maxTables = Long.parseLong(value); + } catch (NumberFormatException exp) { + throw new DoNotRetryIOException("NumberFormatException while getting max tables.", exp); + } + } else { + // The property is not set, so assume its the max long value. + maxTables = Long.MAX_VALUE; + } + return maxTables; + } + + public static long getMaxRegions(NamespaceDescriptor ns) throws IOException { + String value = ns.getConfigurationValue(KEY_MAX_REGIONS); + long maxRegions = 0; + if (StringUtils.isNotEmpty(value)) { + try { + maxRegions = Long.parseLong(value); + } catch (NumberFormatException exp) { + throw new DoNotRetryIOException("NumberFormatException while getting max regions.", exp); + } + } else { + // The property is not set, so assume its the max long value. + maxRegions = Long.MAX_VALUE; + } + return maxRegions; + } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/CreateTableHandler.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/CreateTableHandler.java index 24d8c71..bb5c401 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/CreateTableHandler.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/CreateTableHandler.java @@ -163,9 +163,9 @@ public class CreateTableHandler extends EventHandler { public void process() { TableName tableName = this.hTableDescriptor.getTableName(); LOG.info("Create table " + tableName); - + HMaster master = ((HMaster) this.server); try { - final MasterCoprocessorHost cpHost = ((HMaster) this.server).getCoprocessorHost(); + final MasterCoprocessorHost cpHost = master.getCoprocessorHost(); if (cpHost != null) { cpHost.preCreateTableHandler(this.hTableDescriptor, this.newRegions); } @@ -182,6 +182,14 @@ public class CreateTableHandler extends EventHandler { } } catch (Throwable e) { LOG.error("Error trying to create the table " + tableName, e); + if (master.isInitialized()) { + try { + master.getMasterQuotaManager().removeTableFromNamespaceQuota( + hTableDescriptor.getTableName()); + } catch (IOException e1) { + LOG.error("Error trying to update namespace quota " + e1); + } + } completed(e); } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/DeleteTableHandler.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/DeleteTableHandler.java index 474da0a..411d93e 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/DeleteTableHandler.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/handler/DeleteTableHandler.java @@ -139,6 +139,7 @@ public class DeleteTableHandler extends TableEventHandler { if (cpHost != null) { cpHost.postDeleteTableHandler(this.tableName); } + ((HMaster) this.server).getMasterQuotaManager().removeTableFromNamespaceQuota(tableName); } /** diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceAuditor.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceAuditor.java new file mode 100644 index 0000000..589b2d4 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceAuditor.java @@ -0,0 +1,138 @@ +/** + * 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.namespace; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.DoNotRetryIOException; +import org.apache.hadoop.hbase.HBaseIOException; +import org.apache.hadoop.hbase.HRegionInfo; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.TableExistsException; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.catalog.MetaReader; +import org.apache.hadoop.hbase.classification.InterfaceAudience; +import org.apache.hadoop.hbase.master.MasterServices; + +import com.google.common.annotations.VisibleForTesting; + +/** + * The Class NamespaceAuditor performs checks to ensure operations like table creation and region + * splitting preserve namespace quota. The namespace quota can be specified while namespace + * creation. + */ +@InterfaceAudience.Private +public class NamespaceAuditor { + private static final Log LOG = LogFactory.getLog(NamespaceAuditor.class); + static final String NS_AUDITOR_INIT_TIMEOUT = "hbase.namespace.auditor.init.timeout"; + static final int DEFAULT_NS_AUDITOR_INIT_TIMEOUT = 120000; + private NamespaceStateManager stateManager; + private MasterServices masterServices; + + public NamespaceAuditor(MasterServices masterServices) { + this.masterServices = masterServices; + stateManager = new NamespaceStateManager(masterServices, masterServices.getZooKeeper()); + } + + public void start() throws IOException { + stateManager.start(); + LOG.info("NamespaceAuditor started."); + } + + /** + * Check quota to create table. We add the table information to namespace state cache, assuming + * the operation will pass. If the operation fails, then the next time namespace state chore runs + * namespace state cache will be corrected. + * @param tName - The table name to check quota. + * @param regions - Number of regions that will be added. + * @throws IOException Signals that an I/O exception has occurred. + */ + public void checkQuotaToCreateTable(TableName tName, int regions) throws IOException { + if (isInitialized()) { + // We do this check to fail fast. + if (MetaReader.tableExists(masterServices.getCatalogTracker(), tName)) { + throw new TableExistsException(tName); + } + stateManager.checkAndUpdateNamespaceTableCount(tName, regions); + } else { + checkTableTypeAndThrowException(tName); + } + } + + private void checkTableTypeAndThrowException(TableName name) throws IOException { + if (name.isSystemTable()) { + LOG.debug("Namespace auditor checks not performed for table " + name.getNameAsString()); + } else { + throw new HBaseIOException(name + + " is being created even before namespace auditor has been initialized."); + } + } + + public void checkQuotaToSplitRegion(HRegionInfo hri) throws IOException { + if (!isInitialized()) { + throw new IOException( + "Split operation is being performed even before namespace auditor is initialized."); + } else if (!stateManager.checkAndUpdateNamespaceRegionCount(hri.getTable(), + hri.getRegionName(), 1)) { + throw new DoNotRetryIOException("Region split not possible for :" + hri.getEncodedName() + + " as quota limits are exceeded "); + } + } + + public void updateQuotaForRegionMerge(HRegionInfo hri) throws IOException { + if (isInitialized()) { + throw new IOException( + "Merge operation is being performed even before namespace auditor is initialized."); + } else if (!stateManager + .checkAndUpdateNamespaceRegionCount(hri.getTable(), hri.getRegionName(), -1)) { + throw new DoNotRetryIOException("Region split not possible for :" + hri.getEncodedName() + + " as quota limits are exceeded "); + } + } + + public void addNamespace(NamespaceDescriptor ns) throws IOException { + stateManager.addNamespace(ns.getName()); + } + + public void deleteNamespace(String namespace) throws IOException { + stateManager.deleteNamespace(namespace); + } + + public void removeFromNamespaceUsage(TableName tableName) throws IOException { + stateManager.removeTable(tableName); + } + + public void removeRegionFromNamespaceUsage(HRegionInfo hri) throws IOException { + stateManager.removeRegionFromTable(hri); + } + + /** + * @param namespace The name of the namespace + * @return An instance of NamespaceTableAndRegionInfo + */ + @VisibleForTesting + public NamespaceTableAndRegionInfo getState(String namespace) { + if (isInitialized()) { + return stateManager.getState(namespace); + } + return null; + } + + /** + * Checks if namespace auditor is initialized. Used only for testing. + * @return true, if is initialized + */ + public boolean isInitialized() { + return this.masterServices.isInitialized() && stateManager.isInitialized(); + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceStateManager.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceStateManager.java new file mode 100644 index 0000000..48f5854 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceStateManager.java @@ -0,0 +1,299 @@ +/** + * 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.namespace; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.DoNotRetryIOException; +import org.apache.hadoop.hbase.HRegionInfo; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.RegionTransition; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.classification.InterfaceAudience; +import org.apache.hadoop.hbase.client.MetaScanner; +import org.apache.hadoop.hbase.exceptions.DeserializationException; +import org.apache.hadoop.hbase.executor.EventType; +import org.apache.hadoop.hbase.master.MasterServices; +import org.apache.hadoop.hbase.master.TableNamespaceManager; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.zookeeper.ZKAssign; +import org.apache.hadoop.hbase.zookeeper.ZKUtil; +import org.apache.hadoop.hbase.zookeeper.ZooKeeperListener; +import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.data.Stat; + +/** + * NamespaceStateManager manages state (in terms of quota) of all the namespaces. It contains a + * cache which is updated based on the hooks in the NamespaceAuditor class. + */ +@InterfaceAudience.Private +class NamespaceStateManager extends ZooKeeperListener { + + private static Log LOG = LogFactory.getLog(NamespaceStateManager.class); + private ConcurrentMap nsStateCache; + private MasterServices master; + private volatile boolean initialized = false; + + public NamespaceStateManager(MasterServices masterServices, ZooKeeperWatcher zkw) { + super(zkw); + nsStateCache = new ConcurrentHashMap(); + master = masterServices; + } + + /** + * Starts the NamespaceStateManager. The boot strap of cache is done in the post master start hook + * of the NamespaceAuditor class. + * @throws IOException Signals that an I/O exception has occurred. + */ + public void start() throws IOException { + LOG.info("Namespace State Manager started."); + initialize(); + watcher.registerListenerFirst(this); + } + + /** + * Gets an instance of NamespaceTableAndRegionInfo associated with namespace. + * @param The name of the namespace + * @return An instance of NamespaceTableAndRegionInfo. + */ + public NamespaceTableAndRegionInfo getState(String name) { + return nsStateCache.get(name); + } + + /** + * Check if adding a region violates namespace quota, if not update namespace cache. + * @param TableName + * @param regionName + * @param incr + * @return true, if region can be added to table. + * @throws IOException Signals that an I/O exception has occurred. + */ + synchronized boolean checkAndUpdateNamespaceRegionCount(TableName name, byte[] regionName, + int incr) throws IOException { + String namespace = name.getNamespaceAsString(); + NamespaceDescriptor nspdesc = getNamespaceDescriptor(namespace); + if (nspdesc != null) { + NamespaceTableAndRegionInfo currentStatus; + currentStatus = getState(namespace); + if (incr > 0 + && currentStatus.getRegionCount() >= TableNamespaceManager.getMaxRegions(nspdesc)) { + LOG.warn("The region " + Bytes.toStringBinary(regionName) + + " cannot be created. The region count will exceed quota on the namespace. " + + "This may be transient, please retry later if there are any ongoing split" + + " operations in the namespace."); + return false; + } + NamespaceTableAndRegionInfo nsInfo = nsStateCache.get(namespace); + if (nsInfo != null) { + nsInfo.incRegionCountForTable(name, incr); + } else { + LOG.warn("Namespace state found null for namespace : " + namespace); + } + } + return true; + } + + /** + * Check and update region count for an existing table. To handle scenarios like restore snapshot + * @param TableName name of the table for region count needs to be checked and updated + * @param incr count of regions + * @throws DoNotRetryIOException if quota exceeds for the number of regions allowed in a + * namespace + * @throws IOException Signals that an I/O exception has occurred. + */ + synchronized void checkAndUpdateNamespaceRegionCount(TableName name, int incr) + throws IOException { + String namespace = name.getNamespaceAsString(); + NamespaceDescriptor nspdesc = getNamespaceDescriptor(namespace); + if (nspdesc != null) { + NamespaceTableAndRegionInfo currentStatus = getState(namespace); + int regionCountOfTable = currentStatus.getRegionCountOfTable(name); + if ((currentStatus.getRegionCount() - regionCountOfTable + incr) > TableNamespaceManager + .getMaxRegions(nspdesc)) { + throw new DoNotRetryIOException("The table " + name.getNameAsString() + + " region count cannot be updated as it would exceed maximum number " + + "of regions allowed in the namespace. The total number of regions permitted is " + + TableNamespaceManager.getMaxRegions(nspdesc)); + } + currentStatus.removeTable(name); + currentStatus.addTable(name, incr); + } + } + + private NamespaceDescriptor getNamespaceDescriptor(String namespaceAsString) { + try { + return this.master.getNamespaceDescriptor(namespaceAsString); + } catch (IOException e) { + LOG.error("Error while fetching namespace descriptor for namespace : " + namespaceAsString); + return null; + } + } + + synchronized void checkAndUpdateNamespaceTableCount(TableName table, int numRegions) + throws IOException { + String namespace = table.getNamespaceAsString(); + NamespaceDescriptor nspdesc = getNamespaceDescriptor(namespace); + if (nspdesc != null) { + NamespaceTableAndRegionInfo currentStatus; + currentStatus = getState(nspdesc.getName()); + if ((currentStatus.getTables().size()) >= TableNamespaceManager.getMaxTables(nspdesc)) { + throw new DoNotRetryIOException("The table " + table.getNameAsString() + + "cannot be created as it would exceed maximum number of tables allowed " + + " in the namespace. The total number of tables permitted is " + + TableNamespaceManager.getMaxTables(nspdesc)); + } + if ((currentStatus.getRegionCount() + numRegions) > TableNamespaceManager + .getMaxRegions(nspdesc)) { + throw new DoNotRetryIOException("The table " + table.getNameAsString() + + " is not allowed to have " + numRegions + + " regions. The total number of regions permitted is only " + + TableNamespaceManager.getMaxRegions(nspdesc) + ", while current region count is " + + currentStatus.getRegionCount() + + ". This may be transient, please retry later if there are any" + + " ongoing split operations in the namespace."); + } + } else { + throw new IOException("Namespace Descriptor found null for " + namespace + + " This is unexpected."); + } + addTable(table, numRegions); + } + + NamespaceTableAndRegionInfo addNamespace(String namespace) { + if (!nsStateCache.containsKey(namespace)) { + NamespaceTableAndRegionInfo a1 = new NamespaceTableAndRegionInfo(namespace); + nsStateCache.put(namespace, a1); + } + return nsStateCache.get(namespace); + } + + /** + * Delete the namespace state. + * @param An instance of NamespaceTableAndRegionInfo + */ + void deleteNamespace(String namespace) { + this.nsStateCache.remove(namespace); + } + + private void addTable(TableName tableName, int regionCount) throws IOException { + NamespaceTableAndRegionInfo info = nsStateCache.get(tableName.getNamespaceAsString()); + if (info != null) { + info.addTable(tableName, regionCount); + } else { + throw new IOException("Bad state : Namespace quota information not found for namespace : " + + tableName.getNamespaceAsString()); + } + } + + synchronized void removeTable(TableName tableName) { + NamespaceTableAndRegionInfo info = nsStateCache.get(tableName.getNamespaceAsString()); + if (info != null) { + info.removeTable(tableName); + } + } + + /** + * Initialize namespace state cache by scanning meta table. + */ + private void initialize() throws IOException { + List namespaces = this.master.listNamespaceDescriptors(); + for (NamespaceDescriptor namespace : namespaces) { + addNamespace(namespace.getName()); + List tables = this.master.listTableNamesByNamespace(namespace.getName()); + for (TableName table : tables) { + if (!table.isSystemTable()) { + int regionCount = 0; + Map regions = MetaScanner.allTableRegions( + this.master.getConfiguration(), null, table, true); + for (HRegionInfo info : regions.keySet()) { + if (!info.isSplit()) { + regionCount++; + } + } + addTable(table, regionCount); + } + } + } + LOG.info("Finished updating state of " + nsStateCache.size() + " namespaces. "); + initialized = true; + } + + boolean isInitialized() { + return initialized; + } + + public synchronized void removeRegionFromTable(HRegionInfo hri) throws IOException { + String namespace = hri.getTable().getNamespaceAsString(); + NamespaceTableAndRegionInfo nsInfo = nsStateCache.get(namespace); + if (nsInfo != null) { + nsInfo.decrementRegionCountForTable(hri.getTable(), 1); + } else { + throw new IOException("Namespace state found null for namespace : " + namespace); + } + } + + @Override + public void nodeCreated(String path) { + checkSplittingOrMergingNode(path); + } + + @Override + public void nodeChildrenChanged(String path) { + checkSplittingOrMergingNode(path); + } + + private void checkSplittingOrMergingNode(String path) { + String msg = "Error reading data from zookeeper, "; + try { + if (path.startsWith(watcher.assignmentZNode)) { + List children = + ZKUtil.listChildrenAndWatchForNewChildren(watcher, watcher.assignmentZNode); + if (children != null) { + for (String child : children) { + Stat stat = new Stat(); + byte[] data = + ZKAssign.getDataAndWatch(watcher, ZKUtil.joinZNode(watcher.assignmentZNode, child), + stat); + if (data != null) { + RegionTransition rt = RegionTransition.parseFrom(data); + if (rt.getEventType().equals(EventType.RS_ZK_REQUEST_REGION_SPLIT)) { + TableName table = HRegionInfo.getTable(rt.getRegionName()); + if (!checkAndUpdateNamespaceRegionCount(table, rt.getRegionName(), 1)) { + ZKUtil.deleteNode(watcher, ZKUtil.joinZNode(watcher.assignmentZNode, child)); + } + } else if (rt.getEventType().equals(EventType.RS_ZK_REQUEST_REGION_MERGE)) { + TableName table = HRegionInfo.getTable(rt.getRegionName()); + checkAndUpdateNamespaceRegionCount(table, rt.getRegionName(), -1); + } + } + } + } + } + } catch (KeeperException ke) { + LOG.error(msg, ke); + watcher.abort(msg, ke); + } catch (DeserializationException e) { + LOG.error(msg, e); + watcher.abort(msg, e); + } catch (IOException e) { + LOG.error(msg, e); + watcher.abort(msg, e); + } + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceTableAndRegionInfo.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceTableAndRegionInfo.java new file mode 100644 index 0000000..6fea0b6 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/namespace/NamespaceTableAndRegionInfo.java @@ -0,0 +1,109 @@ +/** + * 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.namespace; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hadoop.hbase.TableName; + +import com.google.common.base.Joiner; +import org.apache.hadoop.hbase.classification.InterfaceAudience; + +/** + * NamespaceTableAndRegionInfo is a helper class that contains information about current state of + * tables and regions in a namespace. + */ +@InterfaceAudience.Private +class NamespaceTableAndRegionInfo { + private String name; + private Map tableAndRegionInfo; + + public NamespaceTableAndRegionInfo(String namespace) { + this.name = namespace; + this.tableAndRegionInfo = new HashMap(); + } + + /** + * Gets the name of the namespace. + * @return name of the namespace. + */ + String getName() { + return name; + } + + /** + * Gets the set of table names belonging to namespace. + * @return A set of table names. + */ + synchronized Set getTables() { + return this.tableAndRegionInfo.keySet(); + } + + /** + * Gets the total number of regions in namespace. + * @return the region count + */ + synchronized int getRegionCount() { + int regionCount = 0; + for (Entry entry : this.tableAndRegionInfo.entrySet()) { + regionCount = regionCount + entry.getValue().get(); + } + return regionCount; + } + + synchronized int getRegionCountOfTable(TableName tableName) { + if (tableAndRegionInfo.containsKey(tableName)) { + return this.tableAndRegionInfo.get(tableName).get(); + } else { + return -1; + } + } + + synchronized boolean containsTable(TableName tableName) { + return this.tableAndRegionInfo.containsKey(tableName); + } + + synchronized void addTable(TableName tableName, int regionCount) { + if (!name.equalsIgnoreCase(tableName.getNamespaceAsString())) { + throw new IllegalStateException("Table : " + tableName + " does not belong to namespace " + + name); + } + if (!tableAndRegionInfo.containsKey(tableName)) { + tableAndRegionInfo.put(tableName, new AtomicInteger(regionCount)); + } else { + throw new IllegalStateException("Table already in the cache " + tableName); + } + } + + synchronized void removeTable(TableName tableName) { + tableAndRegionInfo.remove(tableName); + } + + synchronized int incRegionCountForTable(TableName tableName, int count) { + return tableAndRegionInfo.get(tableName).addAndGet(count); + } + + synchronized int decrementRegionCountForTable(TableName tableName, int count) { + return tableAndRegionInfo.get(tableName).decrementAndGet(); + } + + @Override + public String toString() { + Joiner.MapJoiner mapJoiner = Joiner.on(',').withKeyValueSeparator("="); + return "NamespaceTableAndRegionInfo [name=" + name + ", tableAndRegionInfo=" + + mapJoiner.join(tableAndRegionInfo) + "]"; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/MasterQuotaManager.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/MasterQuotaManager.java new file mode 100644 index 0000000..7c49c7e --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/MasterQuotaManager.java @@ -0,0 +1,131 @@ +/** + * 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.quotas; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HRegionInfo; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.classification.InterfaceAudience; +import org.apache.hadoop.hbase.classification.InterfaceStability; +import org.apache.hadoop.hbase.master.MasterServices; +import org.apache.hadoop.hbase.namespace.NamespaceAuditor; + +/** + * Master Quota Manager. + * It is responsible for initialize the namespace auditor during start up. + * The quota information is maintained as part of the namespace descriptor. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public class MasterQuotaManager implements RegionStateListener { + private static final Log LOG = LogFactory.getLog(MasterQuotaManager.class); + public static final String QUOTA_CONF_KEY = "hbase.quota.enabled"; + private static final boolean QUOTA_ENABLED_DEFAULT = false; + private final MasterServices masterServices; + private boolean enabled = false; + private NamespaceAuditor namespaceQuotaManager; + + public MasterQuotaManager(final MasterServices masterServices) { + this.masterServices = masterServices; + } + + public void start() throws IOException { + // If the user doesn't want the quota support skip all the initializations. + if (!isQuotaEnabled(masterServices.getConfiguration())) { + LOG.info("Quota support disabled"); + return; + } + + LOG.info("Initializing quota support"); + namespaceQuotaManager = new NamespaceAuditor(masterServices); + namespaceQuotaManager.start(); + enabled = true; + } + + public void stop() { + } + + public boolean isQuotaEnabled() { + return enabled && namespaceQuotaManager.isInitialized(); + } + + public void setNamespaceQuota(NamespaceDescriptor desc) throws IOException { + if (enabled) { + this.namespaceQuotaManager.addNamespace(desc); + } + } + + public void removeNamespaceQuota(String namespace) throws IOException { + if (enabled) { + this.namespaceQuotaManager.deleteNamespace(namespace); + } + } + + public void checkNamespaceTableAndRegionQuota(TableName tName, int regions) throws IOException { + if (enabled) { + namespaceQuotaManager.checkQuotaToCreateTable(tName, regions); + } + } + + public void onRegionMerged(HRegionInfo hri) throws IOException { + if (enabled) { + namespaceQuotaManager.updateQuotaForRegionMerge(hri); + } + } + + public void onRegionSplit(HRegionInfo hri) throws IOException { + if (enabled) { + namespaceQuotaManager.checkQuotaToSplitRegion(hri); + } + } + + /** + * Remove table from namespace quota. + * + * @param tName - The table name to update quota usage. + * @throws IOException Signals that an I/O exception has occurred. + */ + public void removeTableFromNamespaceQuota(TableName tName) throws IOException { + if (enabled) { + namespaceQuotaManager.removeFromNamespaceUsage(tName); + } + } + + public NamespaceAuditor getNamespaceQuotaManager() { + return this.namespaceQuotaManager; + } + + @Override + public void onRegionSplitReverted(HRegionInfo hri) throws IOException { + if (enabled) { + this.namespaceQuotaManager.removeRegionFromNamespaceUsage(hri); + } + } + + /** Returns true if the support for quota is enabled */ + public static boolean isQuotaEnabled(final Configuration conf) { + return conf.getBoolean(QUOTA_CONF_KEY, QUOTA_ENABLED_DEFAULT); + } +} + diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/RegionStateListener.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/RegionStateListener.java new file mode 100644 index 0000000..7d4ad19 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/RegionStateListener.java @@ -0,0 +1,44 @@ +/** + * 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.quotas; + +import java.io.IOException; + +import org.apache.hadoop.hbase.HRegionInfo; +import org.apache.hadoop.hbase.classification.InterfaceAudience; + +/** + * The listener interface for receiving region state events. + */ +@InterfaceAudience.Private +public interface RegionStateListener { + + /** + * Process region split event. + * @param hri An instance of HRegionInfo + * @throws IOException + */ + void onRegionSplit(HRegionInfo hri) throws IOException; + + /** + * Process region split reverted event. + * @param hri An instance of HRegionInfo + * @throws IOException Signals that an I/O exception has occurred. + */ + void onRegionSplitReverted(HRegionInfo hri) throws IOException; + + /** + * Process region merge event. + * @param hri An instance of HRegionInfo + * @throws IOException + */ + void onRegionMerged(HRegionInfo hri) throws IOException; +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestCatalogJanitor.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestCatalogJanitor.java index d39fb86..c67afc6 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestCatalogJanitor.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/TestCatalogJanitor.java @@ -63,6 +63,7 @@ import org.apache.hadoop.hbase.protobuf.generated.AdminProtos; import org.apache.hadoop.hbase.protobuf.generated.ClientProtos; import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutateRequest; import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.MutateResponse; +import org.apache.hadoop.hbase.quotas.MasterQuotaManager; import org.apache.hadoop.hbase.regionserver.HStore; import org.apache.hadoop.hbase.testclassification.SmallTests; import org.apache.hadoop.hbase.util.Bytes; @@ -391,6 +392,12 @@ public class TestCatalogJanitor { // Auto-generated method stub return false; } + + @Override + public MasterQuotaManager getMasterQuotaManager() { + // TODO Auto-generated method stub + return null; + } } @Test diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/namespace/TestNamespaceAuditor.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/namespace/TestNamespaceAuditor.java new file mode 100644 index 0000000..972ea86 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/namespace/TestNamespaceAuditor.java @@ -0,0 +1,601 @@ +/** + * 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.namespace; + +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.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.Coprocessor; +import org.apache.hadoop.hbase.CoprocessorEnvironment; +import org.apache.hadoop.hbase.DoNotRetryIOException; +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.HColumnDescriptor; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.HRegionInfo; +import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.MiniHBaseCluster; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.Waiter; +import org.apache.hadoop.hbase.Waiter.Predicate; +import org.apache.hadoop.hbase.client.HBaseAdmin; +import org.apache.hadoop.hbase.client.HTable; +import org.apache.hadoop.hbase.client.Mutation; +import org.apache.hadoop.hbase.coprocessor.BaseMasterObserver; +import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver; +import org.apache.hadoop.hbase.coprocessor.BaseRegionServerObserver; +import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; +import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment; +import org.apache.hadoop.hbase.coprocessor.ObserverContext; +import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment; +import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessorEnvironment; +import org.apache.hadoop.hbase.coprocessor.RegionServerObserver; +import org.apache.hadoop.hbase.mapreduce.TableInputFormatBase; +import org.apache.hadoop.hbase.master.HMaster; +import org.apache.hadoop.hbase.master.RegionState; +import org.apache.hadoop.hbase.master.RegionStates; +import org.apache.hadoop.hbase.master.TableNamespaceManager; +import org.apache.hadoop.hbase.protobuf.generated.AdminProtos.GetRegionInfoResponse.CompactionState; +import org.apache.hadoop.hbase.quotas.MasterQuotaManager; +import org.apache.hadoop.hbase.regionserver.HRegion; +import org.apache.hadoop.hbase.regionserver.HRegionServer; +import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.zookeeper.KeeperException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import com.google.common.collect.Sets; + +@Category(MediumTests.class) +public class TestNamespaceAuditor { + private static final Log LOG = LogFactory.getLog(TestNamespaceAuditor.class); + private static final HBaseTestingUtility UTIL = new HBaseTestingUtility(); + private static HBaseAdmin ADMIN; + private String prefix = "TestNamespaceAuditor"; + + @BeforeClass + public static void before() throws Exception { + UTIL.getConfiguration().setBoolean("hbase.assignment.usezk", false); + setupOnce(); + } + + public static void setupOnce() throws Exception, IOException { + Configuration conf = UTIL.getConfiguration(); + conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, CustomObserver.class.getName()); + conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, MasterSyncObserver.class.getName()); + conf.setBoolean(MasterQuotaManager.QUOTA_CONF_KEY, true); + conf.setClass("hbase.coprocessor.regionserver.classes", CPRegionServerObserver.class, + RegionServerObserver.class); + UTIL.startMiniCluster(1, 1); + waitForQuotaEnabled(); + ADMIN = UTIL.getHBaseAdmin(); + } + + @AfterClass + public static void tearDown() throws Exception { + UTIL.shutdownMiniCluster(); + } + + @After + public void cleanup() throws Exception, KeeperException { + for (HTableDescriptor table : ADMIN.listTables()) { + ADMIN.disableTable(table.getTableName()); + deleteTable(table.getTableName()); + } + for (NamespaceDescriptor ns : ADMIN.listNamespaceDescriptors()) { + if (ns.getName().startsWith(prefix)) { + ADMIN.deleteNamespace(ns.getName()); + } + } + assertTrue("Quota manager not enabled", UTIL.getHBaseCluster().getMaster() + .getMasterQuotaManager().isQuotaEnabled()); + } + + @Test(timeout = 60000) + public void testTableOperations() throws Exception { + String nsp = prefix + "_np2"; + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(nsp) + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "5") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); + ADMIN.createNamespace(nspDesc); + assertNotNull("Namespace descriptor found null.", ADMIN.getNamespaceDescriptor(nsp)); + assertEquals(ADMIN.listNamespaceDescriptors().length, 3); + HTableDescriptor tableDescOne = + new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table1")); + tableDescOne.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY_STR)); + HTableDescriptor tableDescTwo = + new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table2")); + tableDescTwo.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY_STR)); + HTableDescriptor tableDescThree = + new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table3")); + tableDescThree.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY_STR)); + ADMIN.createTable(tableDescOne); + boolean constraintViolated = false; + try { + ADMIN.createTable(tableDescTwo, Bytes.toBytes("AAA"), Bytes.toBytes("ZZZ"), 5); + } catch (Exception exp) { + assertTrue(exp instanceof IOException); + constraintViolated = true; + } finally { + assertTrue("Constraint not violated for table " + tableDescTwo.getTableName(), + constraintViolated); + } + ADMIN.createTable(tableDescTwo, Bytes.toBytes("AAA"), Bytes.toBytes("ZZZ"), 4); + NamespaceTableAndRegionInfo nspState = getQuotaManager().getState(nsp); + assertNotNull(nspState); + assertTrue(nspState.getTables().size() == 2); + assertTrue(nspState.getRegionCount() == 5); + constraintViolated = false; + try { + ADMIN.createTable(tableDescThree); + } catch (Exception exp) { + assertTrue(exp instanceof IOException); + constraintViolated = true; + } finally { + assertTrue("Constraint not violated for table " + tableDescThree.getTableName(), + constraintViolated); + } + } + + @Test + public void testValidQuotas() throws Exception { + boolean exceptionCaught = false; + FileSystem fs = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getFileSystem(); + Path rootDir = UTIL.getHBaseCluster().getMaster().getMasterFileSystem().getRootDir(); + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(prefix + "vq1") + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "hihdufh") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); + try { + ADMIN.createNamespace(nspDesc); + } catch (Exception exp) { + LOG.warn(exp); + exceptionCaught = true; + } finally { + assertTrue(exceptionCaught); + assertFalse(fs.exists(FSUtils.getNamespaceDir(rootDir, nspDesc.getName()))); + } + nspDesc = + NamespaceDescriptor.create(prefix + "vq2") + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "-456") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); + try { + ADMIN.createNamespace(nspDesc); + } catch (Exception exp) { + LOG.warn(exp); + exceptionCaught = true; + } finally { + assertTrue(exceptionCaught); + assertFalse(fs.exists(FSUtils.getNamespaceDir(rootDir, nspDesc.getName()))); + } + nspDesc = + NamespaceDescriptor.create(prefix + "vq3") + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "10") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "sciigd").build(); + try { + ADMIN.createNamespace(nspDesc); + } catch (Exception exp) { + LOG.warn(exp); + exceptionCaught = true; + } finally { + assertTrue(exceptionCaught); + assertFalse(fs.exists(FSUtils.getNamespaceDir(rootDir, nspDesc.getName()))); + } + nspDesc = + NamespaceDescriptor.create(prefix + "vq4") + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "10") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "-1500").build(); + try { + ADMIN.createNamespace(nspDesc); + } catch (Exception exp) { + LOG.warn(exp); + exceptionCaught = true; + } finally { + assertTrue(exceptionCaught); + assertFalse(fs.exists(FSUtils.getNamespaceDir(rootDir, nspDesc.getName()))); + } + } + + @Test + public void testDeleteTable() throws Exception { + String namespace = prefix + "_dummy"; + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(namespace) + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "100") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "3").build(); + ADMIN.createNamespace(nspDesc); + assertNotNull("Namespace descriptor found null.", ADMIN.getNamespaceDescriptor(namespace)); + NamespaceTableAndRegionInfo stateInfo = getNamespaceState(nspDesc.getName()); + assertNotNull("Namespace state found null for " + namespace, stateInfo); + HTableDescriptor tableDescOne = + new HTableDescriptor(TableName.valueOf(namespace + TableName.NAMESPACE_DELIM + "table1")); + tableDescOne.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY)); + HTableDescriptor tableDescTwo = + new HTableDescriptor(TableName.valueOf(namespace + TableName.NAMESPACE_DELIM + "table2")); + tableDescTwo.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY)); + ADMIN.createTable(tableDescOne); + ADMIN.createTable(tableDescTwo, Bytes.toBytes("AAA"), Bytes.toBytes("ZZZ"), 5); + stateInfo = getNamespaceState(nspDesc.getName()); + assertNotNull("Namespace state found to be null.", stateInfo); + assertEquals(2, stateInfo.getTables().size()); + assertEquals(5, stateInfo.getRegionCountOfTable(tableDescTwo.getTableName())); + assertEquals(6, stateInfo.getRegionCount()); + ADMIN.disableTable(tableDescOne.getTableName()); + deleteTable(tableDescOne.getTableName()); + stateInfo = getNamespaceState(nspDesc.getName()); + assertNotNull("Namespace state found to be null.", stateInfo); + assertEquals(5, stateInfo.getRegionCount()); + assertEquals(1, stateInfo.getTables().size()); + ADMIN.disableTable(tableDescTwo.getTableName()); + deleteTable(tableDescTwo.getTableName()); + ADMIN.deleteNamespace(namespace); + stateInfo = getNamespaceState(namespace); + assertNull("Namespace state not found to be null.", stateInfo); + } + + public static class CPRegionServerObserver extends BaseRegionServerObserver { + private volatile boolean shouldFailMerge = false; + + public void failMerge(boolean fail) { + shouldFailMerge = fail; + } + + private boolean triggered = false; + + public synchronized void waitUtilTriggered() throws InterruptedException { + while (!triggered) { + wait(); + } + } + + @Override + public synchronized void preMerge(ObserverContext ctx, + HRegion regionA, HRegion regionB) throws IOException { + triggered = true; + notifyAll(); + if (shouldFailMerge) { + throw new IOException("fail merge"); + } + } + } + + @Test + public void testRegionMerge() throws Exception { + String nsp1 = prefix + "_regiontest"; + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(nsp1) + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "3") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); + ADMIN.createNamespace(nspDesc); + final TableName tableOne = TableName.valueOf(nsp1 + TableName.NAMESPACE_DELIM + "table2"); + byte[] columnFamily = Bytes.toBytes("info"); + HTableDescriptor tableDescOne = new HTableDescriptor(tableOne); + tableDescOne.addFamily(new HColumnDescriptor(columnFamily)); + final int initialRegions = 3; + ADMIN.createTable(tableDescOne, Bytes.toBytes("1"), Bytes.toBytes("2000"), initialRegions); + HTable htable = new HTable(UTIL.getConfiguration(), tableOne); + UTIL.loadNumericRows(htable, Bytes.toBytes("info"), 1000, 1999); + + ADMIN.flush(tableOne.getNameAsString()); + List hris = ADMIN.getTableRegions(tableOne); + Collections.sort(hris); + // merge the two regions + final Set encodedRegionNamesToMerge = + Sets.newHashSet(hris.get(0).getEncodedName(), hris.get(1).getEncodedName()); + ADMIN.mergeRegions(hris.get(0).getEncodedNameAsBytes(), hris.get(1).getEncodedNameAsBytes(), + false); + waitForMergeToComplete(tableOne, encodedRegionNamesToMerge); + hris = ADMIN.getTableRegions(tableOne); + assertEquals(initialRegions - 1, hris.size()); + Collections.sort(hris); + + final HRegionInfo hriToSplit = hris.get(1); + ADMIN.split(tableOne.getName(), Bytes.toBytes("500")); + + UTIL.waitFor(10000, 100, new Predicate() { + + @Override + public boolean evaluate() throws Exception { + RegionStates regionStates = + UTIL.getMiniHBaseCluster().getMaster().getAssignmentManager().getRegionStates(); + for (HRegionInfo hri : ADMIN.getTableRegions(tableOne)) { + if (hri.getEncodedName().equals(hriToSplit.getEncodedName())) { + return false; + } + if (!regionStates.isRegionInState(hri, RegionState.State.OPEN)) { + return false; + } + } + return true; + } + }); + hris = ADMIN.getTableRegions(tableOne); + assertEquals(initialRegions, hris.size()); + Collections.sort(hris); + + // fail region merge through Coprocessor hook + MiniHBaseCluster cluster = UTIL.getHBaseCluster(); + HRegionServer regionServer = cluster.getRegionServer(0); + RegionServerCoprocessorHost cpHost = regionServer.getCoprocessorHost(); + Coprocessor coprocessor = cpHost.findCoprocessor(CPRegionServerObserver.class.getName()); + CPRegionServerObserver regionServerObserver = (CPRegionServerObserver) coprocessor; + regionServerObserver.failMerge(true); + regionServerObserver.triggered = false; + + ADMIN.mergeRegions(hris.get(1).getEncodedNameAsBytes(), hris.get(2).getEncodedNameAsBytes(), + false); + regionServerObserver.waitUtilTriggered(); + hris = ADMIN.getTableRegions(tableOne); + assertEquals(initialRegions, hris.size()); + Collections.sort(hris); + // verify that we cannot split + HRegionInfo hriToSplit2 = hris.get(1); + ADMIN.split(tableOne.getName(), + TableInputFormatBase.getSplitKey(hriToSplit2.getStartKey(), hriToSplit2.getEndKey(), true)); + waitForMergeToComplete(tableOne, encodedRegionNamesToMerge); + assertEquals(initialRegions, ADMIN.getTableRegions(tableOne).size()); + } + + private void waitForMergeToComplete(final TableName tableTwo, + final Set encodedRegionNamesToMerge) throws Exception { + UTIL.waitFor(10000, 100, new Predicate() { + + @Override + public boolean evaluate() throws Exception { + RegionStates regionStates = + UTIL.getMiniHBaseCluster().getMaster().getAssignmentManager().getRegionStates(); + for (HRegionInfo hri : ADMIN.getTableRegions(tableTwo)) { + if (encodedRegionNamesToMerge.contains(hri.getEncodedName())) { + return false; + } + if (!regionStates.isRegionInState(hri, RegionState.State.OPEN)) { + return false; + } + } + return true; + } + }); + } + + @Test + public void testRegionOperations() throws Exception { + String nsp1 = prefix + "_regiontest"; + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(nsp1) + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "2") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); + ADMIN.createNamespace(nspDesc); + boolean constraintViolated = false; + final TableName tableOne = TableName.valueOf(nsp1 + TableName.NAMESPACE_DELIM + "table1"); + byte[] columnFamily = Bytes.toBytes("info"); + HTableDescriptor tableDescOne = new HTableDescriptor(tableOne); + tableDescOne.addFamily(new HColumnDescriptor(columnFamily)); + NamespaceTableAndRegionInfo stateInfo; + try { + ADMIN.createTable(tableDescOne, Bytes.toBytes("1"), Bytes.toBytes("1000"), 7); + } catch (Exception exp) { + assertTrue(exp instanceof DoNotRetryIOException); + LOG.info(exp); + constraintViolated = true; + } finally { + assertTrue(constraintViolated); + } + assertFalse(ADMIN.tableExists(tableOne)); + // This call will pass. + ADMIN.createTable(tableDescOne); + HTable htable = new HTable(UTIL.getConfiguration(), tableOne); + UTIL.loadNumericRows(htable, Bytes.toBytes("info"), 1, 1000); + ADMIN.flush(tableOne.getNameAsString()); + stateInfo = getNamespaceState(nsp1); + assertEquals(1, stateInfo.getTables().size()); + assertEquals(1, stateInfo.getRegionCount()); + restartMaster(); + ADMIN.split(tableOne.getName(), Bytes.toBytes("500")); + HRegion actualRegion = UTIL.getHBaseCluster().getRegions(tableOne).get(0); + CustomObserver observer = + (CustomObserver) actualRegion.getCoprocessorHost().findCoprocessor( + CustomObserver.class.getName()); + assertNotNull(observer); + observer.postSplit.await(); + assertEquals(2, ADMIN.getTableRegions(tableOne).size()); + stateInfo = getNamespaceState(nsp1); + assertEquals(1, stateInfo.getTables().size()); + assertEquals(2, stateInfo.getRegionCount()); + actualRegion = UTIL.getHBaseCluster().getRegions(tableOne).get(0); + observer = + (CustomObserver) actualRegion.getCoprocessorHost().findCoprocessor( + CustomObserver.class.getName()); + assertNotNull(observer); + ADMIN.majorCompact(actualRegion.getRegionName()); + while (ADMIN.getCompactionState(actualRegion.getRegionName()) != CompactionState.NONE) { + Thread.sleep(1000); + } + ADMIN.split( + tableOne.getName(), + getSplitKey(actualRegion.getRegionInfo().getStartKey(), actualRegion.getRegionInfo() + .getEndKey())); + observer.postSplit.await(); + // Make sure no regions have been added. + List hris = ADMIN.getTableRegions(tableOne); + assertEquals(2, hris.size()); + stateInfo = getNamespaceState(nsp1); + assertEquals(1, stateInfo.getTables().size()); + assertEquals(2, stateInfo.getRegionCount()); + assertTrue("split completed", observer.preSplitBeforePONR.getCount() == 1); + + htable.close(); + } + + private NamespaceTableAndRegionInfo getNamespaceState(String namespace) throws KeeperException, + IOException { + return getQuotaManager().getState(namespace); + } + + byte[] getSplitKey(byte[] startKey, byte[] endKey) { + String skey = Bytes.toString(startKey); + int key; + if (StringUtils.isBlank(skey)) { + key = Integer.parseInt(Bytes.toString(endKey)) / 2; + } else { + key = (int) (Integer.parseInt(skey) * 1.5); + } + return Bytes.toBytes("" + key); + } + + public static class CustomObserver extends BaseRegionObserver { + volatile CountDownLatch postSplit; + volatile CountDownLatch preSplitBeforePONR; + + @Override + public void postCompleteSplit(ObserverContext ctx) + throws IOException { + postSplit.countDown(); + } + + @Override + public void preSplitBeforePONR(ObserverContext ctx, + byte[] splitKey, List metaEntries) throws IOException { + preSplitBeforePONR.countDown(); + } + + @Override + public void start(CoprocessorEnvironment e) throws IOException { + postSplit = new CountDownLatch(1); + preSplitBeforePONR = new CountDownLatch(1); + } + } + + @Test + public void testStatePreserve() throws Exception { + final String nsp1 = prefix + "_testStatePreserve"; + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(nsp1) + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "20") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "10").build(); + ADMIN.createNamespace(nspDesc); + TableName tableOne = TableName.valueOf(nsp1 + TableName.NAMESPACE_DELIM + "table1"); + TableName tableTwo = TableName.valueOf(nsp1 + TableName.NAMESPACE_DELIM + "table2"); + TableName tableThree = TableName.valueOf(nsp1 + TableName.NAMESPACE_DELIM + "table3"); + HTableDescriptor tableDescOne = new HTableDescriptor(tableOne); + tableDescOne.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY)); + HTableDescriptor tableDescTwo = new HTableDescriptor(tableTwo); + tableDescTwo.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY)); + HTableDescriptor tableDescThree = new HTableDescriptor(tableThree); + tableDescThree.addFamily(new HColumnDescriptor(HConstants.CATALOG_FAMILY)); + ADMIN.createTable(tableDescOne, Bytes.toBytes("1"), Bytes.toBytes("1000"), 3); + ADMIN.createTable(tableDescTwo, Bytes.toBytes("1"), Bytes.toBytes("1000"), 3); + ADMIN.createTable(tableDescThree, Bytes.toBytes("1"), Bytes.toBytes("1000"), 4); + ADMIN.disableTable(tableThree); + deleteTable(tableThree); + // wait for chore to complete + UTIL.waitFor(1000, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return (getNamespaceState(nsp1).getTables().size() == 2); + } + }); + NamespaceTableAndRegionInfo before = getNamespaceState(nsp1); + restartMaster(); + NamespaceTableAndRegionInfo after = getNamespaceState(nsp1); + assertEquals("Expected: " + before.getTables() + " Found: " + after.getTables(), before + .getTables().size(), after.getTables().size()); + } + + private static void waitForQuotaEnabled() throws Exception { + UTIL.waitFor(60000, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + HMaster master = UTIL.getHBaseCluster().getMaster(); + if (master == null) { + return false; + } + MasterQuotaManager quotaManager = master.getMasterQuotaManager(); + return quotaManager != null && quotaManager.isQuotaEnabled(); + } + }); + } + + private void restartMaster() throws Exception { + UTIL.getHBaseCluster().getMaster(0).stop("Stopping to start again"); + UTIL.getHBaseCluster().waitOnMaster(0); + UTIL.getHBaseCluster().startMaster(); + waitForQuotaEnabled(); + } + + private NamespaceAuditor getQuotaManager() { + return UTIL.getHBaseCluster().getMaster().getMasterQuotaManager().getNamespaceQuotaManager(); + } + + public static class MasterSyncObserver extends BaseMasterObserver { + volatile CountDownLatch tableDeletionLatch; + + @Override + public void preDeleteTable(ObserverContext ctx, + TableName tableName) throws IOException { + tableDeletionLatch = new CountDownLatch(1); + } + + @Override + public void postDeleteTableHandler(final ObserverContext ctx, + TableName tableName) throws IOException { + tableDeletionLatch.countDown(); + } + } + + private void deleteTable(final TableName tableName) throws Exception { + // NOTE: We need a latch because admin is not sync, + // so the postOp coprocessor method may be called after the admin operation returned. + MasterSyncObserver observer = + (MasterSyncObserver) UTIL.getHBaseCluster().getMaster().getCoprocessorHost() + .findCoprocessor(MasterSyncObserver.class.getName()); + ADMIN.deleteTable(tableName); + observer.tableDeletionLatch.await(); + } + + @Test(expected = DoNotRetryIOException.class, timeout = 30000) + public void testExceedTableQuotaInNamespace() throws Exception { + String nsp = prefix + "_testExceedTableQuotaInNamespace"; + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(nsp).addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "1") + .build(); + ADMIN.createNamespace(nspDesc); + assertNotNull("Namespace descriptor found null.", ADMIN.getNamespaceDescriptor(nsp)); + assertEquals(ADMIN.listNamespaceDescriptors().length, 3); + HTableDescriptor tableDescOne = + new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table1")); + HTableDescriptor tableDescTwo = + new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table2")); + ADMIN.createTable(tableDescOne); + ADMIN.createTable(tableDescTwo, Bytes.toBytes("AAA"), Bytes.toBytes("ZZZ"), 4); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/namespace/TestZKLessNamespaceAuditor.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/namespace/TestZKLessNamespaceAuditor.java new file mode 100644 index 0000000..ede19ec --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/namespace/TestZKLessNamespaceAuditor.java @@ -0,0 +1,33 @@ +/** + * 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.namespace; + +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.experimental.categories.Category; + +@Category(MediumTests.class) +public class TestZKLessNamespaceAuditor extends TestNamespaceAuditor { + private static final HBaseTestingUtility UTIL = new HBaseTestingUtility(); + + @BeforeClass + public static void before() throws Exception { + UTIL.getConfiguration().setBoolean("hbase.assignment.usezk", false); + setupOnce(); + } + + @AfterClass + public static void tearDown() throws Exception { + TestNamespaceAuditor.tearDown(); + } +}