You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by pa...@apache.org on 2021/03/06 05:14:47 UTC

[hbase] branch branch-2 updated: HBASE-25492: Create table with RSGroup (#2883)

This is an automated email from the ASF dual-hosted git repository.

pankajkumar pushed a commit to branch branch-2
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2 by this push:
     new 0487760  HBASE-25492: Create table with RSGroup (#2883)
0487760 is described below

commit 04877608297c4d2816d95b6a4a7f53f4a4b19196
Author: Mohammad Arshad <ar...@apache.org>
AuthorDate: Sat Mar 6 10:44:19 2021 +0530

    HBASE-25492: Create table with RSGroup (#2883)
    
    Added new interface in TableDescriptor which allows user to define RSGroup name while creating or modifying a table.
    
    Signed-off-by: Duo Zhang <zh...@apache.org>
    Signed-off-by: Pankaj Kumar<pa...@apache.org>
---
 .../org/apache/hadoop/hbase/HTableDescriptor.java  |   6 +
 .../hadoop/hbase/client/TableDescriptor.java       |   8 +
 .../hbase/client/TableDescriptorBuilder.java       |  25 ++
 .../apache/hadoop/hbase/rsgroup/RSGroupInfo.java   |   1 +
 .../hadoop/hbase/rsgroup/RSGroupAdminEndpoint.java |  72 +++-
 .../hadoop/hbase/rsgroup/RSGroupAdminServer.java   |  76 +++-
 .../rsgroup/TestTableDescriptorWithRSGroup.java    | 413 +++++++++++++++++++++
 7 files changed, 590 insertions(+), 11 deletions(-)

diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java
index 32bf71d..7285846 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/HTableDescriptor.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -987,4 +988,9 @@ public class HTableDescriptor implements TableDescriptor, Comparable<HTableDescr
   protected ModifyableTableDescriptor getDelegateeForModification() {
     return delegatee;
   }
+
+  @Override
+  public Optional<String> getRegionServerGroup() {
+    return delegatee.getRegionServerGroup();
+  }
 }
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptor.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptor.java
index 2ae273d..02015e8 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptor.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptor.java
@@ -23,6 +23,7 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -360,4 +361,11 @@ public interface TableDescriptor {
     }
     return !enabled;
   }
+
+  /**
+   * Get the region server group this table belongs to. The regions of this table will be placed
+   * only on the region servers within this group. If not present, will be placed on
+   * {@link org.apache.hadoop.hbase.rsgroup.RSGroupInfo#DEFAULT_GROUP}.
+   */
+  Optional<String> getRegionServerGroup();
 }
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptorBuilder.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptorBuilder.java
index daab1ca..935078d 100644
--- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptorBuilder.java
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/TableDescriptorBuilder.java
@@ -41,6 +41,7 @@ import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.exceptions.DeserializationException;
 import org.apache.hadoop.hbase.exceptions.HBaseException;
+import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
 import org.apache.hadoop.hbase.security.User;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.hbase.util.PrettyPrinter;
@@ -192,6 +193,9 @@ public class TableDescriptorBuilder {
   private static final Bytes PRIORITY_KEY
           = new Bytes(Bytes.toBytes(PRIORITY));
 
+  private static final Bytes RSGROUP_KEY =
+      new Bytes(Bytes.toBytes(RSGroupInfo.TABLE_DESC_PROP_GROUP));
+
   /**
    * Relative priority of the table used for rpc scheduling
    */
@@ -594,6 +598,17 @@ public class TableDescriptorBuilder {
     return this;
   }
 
+  /**
+   * Set the RSGroup for this table, specified RSGroup must exist before create or modify table.
+   *
+   * @param group rsgroup name
+   * @return a TableDescriptorBuilder
+   */
+  public TableDescriptorBuilder setRegionServerGroup(String group) {
+    desc.setValue(RSGROUP_KEY, group);
+    return this;
+  }
+
   public TableDescriptor build() {
     return new ModifyableTableDescriptor(desc);
   }
@@ -1651,6 +1666,16 @@ public class TableDescriptorBuilder {
     public int getColumnFamilyCount() {
       return families.size();
     }
+
+    @Override
+    public Optional<String> getRegionServerGroup() {
+      Bytes value = values.get(RSGROUP_KEY);
+      if (value != null) {
+        return Optional.of(Bytes.toString(value.get(), value.getOffset(), value.getLength()));
+      } else {
+        return Optional.empty();
+      }
+    }
   }
 
   private static Optional<CoprocessorDescriptor> toCoprocessorDescriptor(String spec) {
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfo.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfo.java
index e183a61..65314cb 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfo.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfo.java
@@ -38,6 +38,7 @@ import org.apache.yetus.audience.InterfaceAudience;
 public class RSGroupInfo {
   public static final String DEFAULT_GROUP = "default";
   public static final String NAMESPACE_DESC_PROP_GROUP = "hbase.rsgroup.name";
+  public static final String TABLE_DESC_PROP_GROUP = "hbase.rsgroup.name";
 
   private final String name;
   // Keep servers in a sorted set so has an expected ordering when displayed.
diff --git a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminEndpoint.java b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminEndpoint.java
index 55cf88d..bf26de1 100644
--- a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminEndpoint.java
+++ b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminEndpoint.java
@@ -485,7 +485,7 @@ public class RSGroupAdminEndpoint implements MasterCoprocessor, MasterObserver {
   }
 
   void assignTableToGroup(TableDescriptor desc) throws IOException {
-    RSGroupInfo rsGroupInfo = groupInfoManager.determineRSGroupInfoForTable(desc.getTableName());
+    RSGroupInfo rsGroupInfo = determineRSGroupInfoForTable(desc);
     if (rsGroupInfo == null) {
       throw new ConstraintException("Default RSGroup for this table " + desc.getTableName()
         + " does not exist.");
@@ -508,21 +508,75 @@ public class RSGroupAdminEndpoint implements MasterCoprocessor, MasterObserver {
     if (desc.getTableName().isSystemTable()) {
       return;
     }
-    RSGroupInfo rsGroupInfo = groupInfoManager.determineRSGroupInfoForTable(desc.getTableName());
+    moveTableToValidRSGroup(desc);
+  }
+
+  @Override
+  public void preModifyTableAction(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    TableName tableName, TableDescriptor currentDescriptor, TableDescriptor newDescriptor)
+    throws IOException {
+    // If table's rsgroup is changed, it must be valid
+    if (!currentDescriptor.getRegionServerGroup().equals(newDescriptor.getRegionServerGroup())) {
+      RSGroupInfo rsGroupInfo = determineRSGroupInfoForTable(newDescriptor);
+      validateRSGroup(newDescriptor, rsGroupInfo);
+    }
+  }
+
+  @Override
+  public void postCompletedModifyTableAction(ObserverContext<MasterCoprocessorEnvironment> ctx,
+    TableName tableName, TableDescriptor oldDescriptor, TableDescriptor currentDescriptor)
+    throws IOException {
+    // If table's rsgroup is changed, move table into the rsgroup.
+    if (!oldDescriptor.getRegionServerGroup().equals(currentDescriptor.getRegionServerGroup())) {
+      RSGroupInfo rsGroupInfo = determineRSGroupInfoForTable(currentDescriptor);
+      moveTableToRSGroup(currentDescriptor, rsGroupInfo);
+    }
+  }
+
+  // Determine and validate rs group then move table to this valid rs group.
+  private void moveTableToValidRSGroup(TableDescriptor desc) throws IOException {
+    RSGroupInfo rsGroupInfo = determineRSGroupInfoForTable(desc);
+    validateRSGroup(desc, rsGroupInfo);
+    moveTableToRSGroup(desc, rsGroupInfo);
+  }
+
+  private void validateRSGroup(TableDescriptor desc, RSGroupInfo rsGroupInfo) throws IOException {
     if (rsGroupInfo == null) {
-      throw new ConstraintException("Default RSGroup for this table " + desc.getTableName()
-        + " does not exist.");
+      throw new ConstraintException(
+        "Default RSGroup for this table " + desc.getTableName() + " does not exist.");
     }
     if (!RSGroupUtil.rsGroupHasOnlineServer(master, rsGroupInfo)) {
-      throw new HBaseIOException("No online servers in the rsgroup " + rsGroupInfo.getName()
-        + " which table " + desc.getTableName().getNameAsString() + " belongs to");
+      throw new HBaseIOException(
+        "No online servers in the rsgroup " + rsGroupInfo.getName() + " which table " + desc
+          .getTableName().getNameAsString() + " belongs to");
     }
-    synchronized (groupInfoManager) {
-      groupInfoManager.moveTables(
-        Collections.singleton(desc.getTableName()), rsGroupInfo.getName());
+  }
+
+  private void moveTableToRSGroup(final TableDescriptor desc, RSGroupInfo rsGroupInfo)
+    throws IOException {
+    // In case of modify table, when rs group is not changed, move is not required.
+    if (!rsGroupInfo.containsTable(desc.getTableName())) {
+      synchronized (groupInfoManager) {
+        groupInfoManager
+          .moveTables(Collections.singleton(desc.getTableName()), rsGroupInfo.getName());
+      }
     }
   }
 
+  private RSGroupInfo determineRSGroupInfoForTable(final TableDescriptor desc) throws IOException {
+    Optional<String> optGroupNameOfTable = desc.getRegionServerGroup();
+    if (optGroupNameOfTable.isPresent()) {
+      final RSGroupInfo rsGroup = groupInfoManager.getRSGroup(optGroupNameOfTable.get());
+      if (rsGroup == null) {
+        // When rs group is set in table descriptor then it must exist
+        throw new ConstraintException(
+            "Region server group " + optGroupNameOfTable.get() + " does not exist.");
+      } else {
+        return rsGroup;
+      }
+    }
+    return groupInfoManager.determineRSGroupInfoForTable(desc.getTableName());
+  }
   // Remove table from its RSGroup.
   @Override
   public void postDeleteTable(ObserverContext<MasterCoprocessorEnvironment> ctx,
diff --git a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminServer.java b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminServer.java
index b284a2b..0c65dbf 100644
--- a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminServer.java
+++ b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupAdminServer.java
@@ -27,11 +27,15 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Future;
+import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.NamespaceDescriptor;
 import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.TableName;
 import org.apache.hadoop.hbase.client.RegionInfo;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
 import org.apache.hadoop.hbase.client.TableState;
 import org.apache.hadoop.hbase.constraint.ConstraintException;
 import org.apache.hadoop.hbase.master.HMaster;
@@ -43,7 +47,9 @@ import org.apache.hadoop.hbase.master.ServerManager;
 import org.apache.hadoop.hbase.master.TableStateManager;
 import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
 import org.apache.hadoop.hbase.master.assignment.RegionStateNode;
+import org.apache.hadoop.hbase.master.procedure.ProcedureSyncWait;
 import org.apache.hadoop.hbase.net.Address;
+import org.apache.hadoop.hbase.procedure2.Procedure;
 import org.apache.hadoop.hbase.util.Pair;
 import org.apache.hbase.thirdparty.com.google.common.collect.Maps;
 import org.apache.yetus.audience.InterfaceAudience;
@@ -459,7 +465,7 @@ public class RSGroupAdminServer implements RSGroupAdmin {
       // targetGroup is null when a table is being deleted. In this case no further
       // action is required.
       if (targetGroup != null) {
-        moveTableRegionsToGroup(tables, rsGroupInfoManager.getRSGroup(targetGroup));
+        modifyOrMoveTables(tables, rsGroupInfoManager.getRSGroup(targetGroup));
       }
     }
   }
@@ -582,7 +588,7 @@ public class RSGroupAdminServer implements RSGroupAdmin {
         rsGroupInfoManager.getRSGroup(srcGroup).getServers(),
         targetGroup, srcGroup);
       //move regions of these tables which are not on group servers
-      moveTableRegionsToGroup(tables, rsGroupInfoManager.getRSGroup(targetGroup));
+      modifyOrMoveTables(tables, rsGroupInfoManager.getRSGroup(targetGroup));
     }
     LOG.info("Move servers and tables done. Severs: {}, Tables: {} => {}", servers, tables,
         targetGroup);
@@ -609,6 +615,11 @@ public class RSGroupAdminServer implements RSGroupAdmin {
   public void renameRSGroup(String oldName, String newName) throws IOException {
     synchronized (rsGroupInfoManager) {
       rsGroupInfoManager.renameRSGroup(oldName, newName);
+      Set<TableDescriptor> updateTables = master.getTableDescriptors().getAll().values().stream()
+        .filter(t -> oldName.equals(t.getRegionServerGroup().orElse(null)))
+        .collect(Collectors.toSet());
+      // Update rs group info into table descriptors
+      modifyTablesAndWaitForCompletion(updateTables, newName);
     }
   }
 
@@ -718,4 +729,65 @@ public class RSGroupAdminServer implements RSGroupAdmin {
       }
     }
   }
+
+  // Modify table or move table's regions
+  void modifyOrMoveTables(Set<TableName> tables, RSGroupInfo targetGroup) throws IOException {
+    Set<TableName> tablesToBeMoved = new HashSet<>(tables.size());
+    Set<TableDescriptor> tablesToBeModified = new HashSet<>(tables.size());
+    // Segregate tables into to be modified or to be moved category
+    for (TableName tableName : tables) {
+      TableDescriptor descriptor = master.getTableDescriptors().get(tableName);
+      if (descriptor == null) {
+        LOG.error(
+          "TableDescriptor of table {} not found. Skipping the region movement of this table.");
+        continue;
+      }
+      if (descriptor.getRegionServerGroup().isPresent()) {
+        tablesToBeModified.add(descriptor);
+      } else {
+        tablesToBeMoved.add(tableName);
+      }
+    }
+    List<Long> procedureIds = null;
+    if (!tablesToBeModified.isEmpty()) {
+      procedureIds = modifyTables(tablesToBeModified, targetGroup.getName());
+    }
+    if (!tablesToBeMoved.isEmpty()) {
+      moveTableRegionsToGroup(tablesToBeMoved, targetGroup);
+    }
+    // By this time moveTableRegionsToGroup is finished, lets wait for modifyTables completion
+    if (procedureIds != null) {
+      waitForProcedureCompletion(procedureIds);
+    }
+  }
+
+  private void modifyTablesAndWaitForCompletion(Set<TableDescriptor> tableDescriptors,
+    String targetGroup) throws IOException {
+    final List<Long> procIds = modifyTables(tableDescriptors, targetGroup);
+    waitForProcedureCompletion(procIds);
+  }
+
+  // Modify table internally moves the regions as well. So separate region movement is not needed
+  private List<Long> modifyTables(Set<TableDescriptor> tableDescriptors, String targetGroup)
+    throws IOException {
+    List<Long> procIds = new ArrayList<>(tableDescriptors.size());
+    for (TableDescriptor oldTd : tableDescriptors) {
+      TableDescriptor newTd =
+        TableDescriptorBuilder.newBuilder(oldTd).setRegionServerGroup(targetGroup).build();
+      procIds.add(master
+        .modifyTable(oldTd.getTableName(), newTd, HConstants.NO_NONCE, HConstants.NO_NONCE));
+    }
+    return procIds;
+  }
+
+  private void waitForProcedureCompletion(List<Long> procIds) throws IOException {
+    for (long procId : procIds) {
+      Procedure<?> proc = master.getMasterProcedureExecutor().getProcedure(procId);
+      if (proc == null) {
+        continue;
+      }
+      ProcedureSyncWait
+        .waitForProcedureToCompleteIOE(master.getMasterProcedureExecutor(), proc, Long.MAX_VALUE);
+    }
+  }
 }
diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestTableDescriptorWithRSGroup.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestTableDescriptorWithRSGroup.java
new file mode 100644
index 0000000..b25daa3
--- /dev/null
+++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestTableDescriptorWithRSGroup.java
@@ -0,0 +1,413 @@
+/*
+ * 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.rsgroup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import org.apache.hadoop.hbase.DoNotRetryIOException;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.Waiter;
+import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
+import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
+import org.apache.hadoop.hbase.client.SnapshotDescription;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.constraint.ConstraintException;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.apache.hbase.thirdparty.com.google.common.collect.Sets;
+
+@Category({ LargeTests.class })
+public class TestTableDescriptorWithRSGroup extends TestRSGroupsBase {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(TestTableDescriptorWithRSGroup.class);
+  private final byte[] familyNameBytes = Bytes.toBytes("f1");
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    setUpTestBeforeClass();
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    tearDownAfterClass();
+  }
+
+  @Before
+  public void beforeMethod() throws Exception {
+    setUpBeforeMethod();
+  }
+
+  @After
+  public void afterMethod() throws Exception {
+    tearDownAfterMethod();
+  }
+
+  @Test
+  public void testCreateTableInTableDescriptorSpecificRSGroup() throws Exception {
+    final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 1);
+    assertEquals(0, newGroup.getTables().size());
+
+    // assertion is done in createTableInRSGroup
+    createTableWithRSGroupDetail(newGroup.getName());
+
+    // Create table should fail if specified rs group does not exist.
+    try {
+      TableDescriptor desc =
+        TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName.getNameAsString() + "_2"))
+          .setRegionServerGroup("nonExistingRSGroup")
+          .setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("f1")).build())
+          .build();
+      admin.createTable(desc, getSpitKeys(6));
+      fail("Should have thrown ConstraintException but no exception thrown.");
+    } catch (ConstraintException e) {
+      assertEquals(e.getMessage(), "Region server group nonExistingRSGroup does not exist.");
+    }
+  }
+
+  private void createTableWithRSGroupDetail(String newGroup) throws Exception {
+    // Create table
+
+    ColumnFamilyDescriptor f1 = ColumnFamilyDescriptorBuilder.newBuilder(familyNameBytes).build();
+    TableDescriptor desc =
+      TableDescriptorBuilder.newBuilder(tableName).setRegionServerGroup(newGroup)
+        .setColumnFamily(f1).build();
+    admin.createTable(desc, getSpitKeys(5));
+
+    TEST_UTIL.waitFor(WAIT_TIMEOUT, (Waiter.Predicate<Exception>) () -> {
+      List<String> regions = getTableRegionMap().get(tableName);
+      if (regions == null) {
+        return false;
+      }
+
+      return getTableRegionMap().get(tableName).size() >= 5;
+    });
+    TableDescriptor descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    Optional<String> regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table is created.",
+      regionServerGroup.isPresent());
+    assertEquals(newGroup, regionServerGroup.get());
+  }
+
+  // moveTables should update rs group info in table descriptor
+  @Test
+  public void testMoveTablesShouldUpdateTableDescriptor() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+
+    createTableWithRSGroupDetail(rsGroup1.getName());
+    TableDescriptor descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    Optional<String> regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table created",
+      regionServerGroup.isPresent());
+    assertEquals(rsGroup1.getName(), regionServerGroup.get());
+
+    // moveTables
+    RSGroupInfo rsGroup2 = addGroup("rsGroup2", 1);
+    rsGroupAdmin.moveTables(Sets.newHashSet(tableName), rsGroup2.getName());
+    descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table moved",
+      regionServerGroup.isPresent());
+    assertEquals(rsGroup2.getName(), regionServerGroup.get());
+  }
+
+  // moveServersAndTables should update rs group info in table descriptor
+  @Test
+  public void testMoveServersAndTablesShouldUpdateTableDescriptor() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 2);
+    assertEquals(0, rsGroup1.getTables().size());
+
+    createTableWithRSGroupDetail(rsGroup1.getName());
+    TableDescriptor descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    Optional<String> regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table created",
+      regionServerGroup.isPresent());
+    assertEquals(rsGroup1.getName(), regionServerGroup.get());
+
+    // moveServersAndTables
+    rsGroupAdmin.moveServersAndTables(Sets.newHashSet(rsGroup1.getServers().iterator().next()),
+      Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP);
+
+    descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table moved",
+      regionServerGroup.isPresent());
+    assertEquals(RSGroupInfo.DEFAULT_GROUP, regionServerGroup.get());
+
+  }
+
+  @Test
+  public void testRenameRSGroupUpdatesTableDescriptor() throws Exception {
+    RSGroupInfo oldGroup = addGroup("oldGroup", 1);
+    assertEquals(0, oldGroup.getTables().size());
+
+    createTableWithRSGroupDetail(oldGroup.getName());
+
+    final String newGroupName = "newGroup";
+    rsGroupAdmin.renameRSGroup(oldGroup.getName(), newGroupName);
+    TableDescriptor descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    Optional<String> regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when rs group renamed",
+      regionServerGroup.isPresent());
+    assertEquals(newGroupName, regionServerGroup.get());
+  }
+
+  @Test
+  public void testCloneSnapshotWithRSGroup() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+    // Creates table in rsGroup1
+    createTableWithRSGroupDetail(rsGroup1.getName());
+
+    // Create snapshot
+    final String snapshotName = "snapShot2";
+    admin.snapshot(snapshotName, tableName);
+    final List<SnapshotDescription> snapshotDescriptions =
+      admin.listSnapshots(Pattern.compile("snapShot2"));
+    assertEquals(1, snapshotDescriptions.size());
+    assertEquals(snapshotName, snapshotDescriptions.get(0).getName());
+
+    // Move table to different rs group then delete the old rs group
+    RSGroupInfo rsGroup2 = addGroup("rsGroup2", 1);
+    rsGroupAdmin.moveTables(Sets.newHashSet(tableName), rsGroup2.getName());
+    rsGroup2 = rsGroupAdmin.getRSGroupInfo(rsGroup2.getName());
+    assertEquals(1, rsGroup2.getTables().size());
+    assertEquals(rsGroup2.getTables().first(), tableName);
+
+    // Clone Snapshot
+    final TableName clonedTable1 = TableName.valueOf(tableName.getNameAsString() + "_1");
+    admin.cloneSnapshot(Bytes.toBytes(snapshotName), clonedTable1);
+
+    // Verify that cloned table is created into old rs group
+    final RSGroupInfo rsGroupInfoOfTable = rsGroupAdmin.getRSGroupInfoOfTable(clonedTable1);
+    assertEquals(rsGroup1.getName(), rsGroupInfoOfTable.getName());
+    TableDescriptor descriptor = admin.getConnection().getTable(clonedTable1).getDescriptor();
+    Optional<String> regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table is cloned.",
+      regionServerGroup.isPresent());
+    assertEquals(rsGroup1.getName(), regionServerGroup.get());
+
+    // Delete table's original rs group, clone should fail.
+    rsGroupAdmin
+      .moveServersAndTables(Sets.newHashSet(rsGroup1.getServers()), Sets.newHashSet(clonedTable1),
+        rsGroup2.getName());
+    rsGroupAdmin.removeRSGroup(rsGroup1.getName());
+    // Clone Snapshot
+    final TableName clonedTable2 = TableName.valueOf(tableName.getNameAsString() + "_2");
+    try {
+      admin.cloneSnapshot(Bytes.toBytes(snapshotName), clonedTable2);
+      fail("Should have thrown ConstraintException but no exception thrown.");
+    } catch (ConstraintException e) {
+      assertTrue(
+        e.getCause().getMessage().contains("Region server group rsGroup1 does not exist."));
+    }
+  }
+
+  // Modify table should validate rs group existence and move rs group if required
+  @Test
+  public void testMoveTablesWhenModifyTableWithNewRSGroup() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+
+    createTableWithRSGroupDetail(rsGroup1.getName());
+    TableDescriptor descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    Optional<String> regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table created",
+      regionServerGroup.isPresent());
+    assertEquals(rsGroup1.getName(), regionServerGroup.get());
+
+    final TableDescriptor newTableDescriptor =
+      TableDescriptorBuilder.newBuilder(descriptor).setRegionServerGroup("rsGroup2").build();
+
+    // ConstraintException as rsGroup2 does not exits
+    try {
+      admin.modifyTable(newTableDescriptor);
+      fail("Should have thrown ConstraintException but no exception thrown.");
+    } catch (ConstraintException e) {
+      assertTrue(
+        e.getCause().getMessage().contains("Region server group rsGroup2 does not exist."));
+    }
+
+    addGroup("rsGroup2", 1);
+    // Table creation should be successful as rsGroup2 exists.
+    admin.modifyTable(newTableDescriptor);
+
+    // old group should not have table mapping now
+    rsGroup1 = rsGroupAdmin.getRSGroupInfo("rsGroup1");
+    assertEquals(0, rsGroup1.getTables().size());
+
+    RSGroupInfo rsGroup2 = rsGroupAdmin.getRSGroupInfo("rsGroup2");
+    assertEquals(1, rsGroup2.getTables().size());
+    descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    regionServerGroup = descriptor.getRegionServerGroup();
+    assertTrue("RSGroup info is not updated into TableDescriptor when table is modified.",
+      regionServerGroup.isPresent());
+    assertEquals("rsGroup2", regionServerGroup.get());
+  }
+
+  @Test
+  public void testTableShouldNotAddedIntoRSGroup_WhenModifyTableFails() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+
+    createTableWithRSGroupDetail(rsGroup1.getName());
+    TableDescriptor descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+
+    RSGroupInfo rsGroup2 = addGroup("rsGroup2", 1);
+    final TableDescriptor newTableDescriptor =
+      TableDescriptorBuilder.newBuilder(descriptor).setRegionServerGroup(rsGroup2.getName())
+        .removeColumnFamily(familyNameBytes).build();
+
+    // Removed family to fail pre-check validation
+    try {
+      admin.modifyTable(newTableDescriptor);
+      fail("Should have thrown DoNotRetryIOException but no exception thrown.");
+    } catch (DoNotRetryIOException e) {
+      assertTrue(
+        e.getCause().getMessage().contains("Table should have at least one column family"));
+    }
+    rsGroup2 = rsGroupAdmin.getRSGroupInfo(rsGroup2.getName());
+    assertEquals("Table must not have moved to RSGroup as table modify failed", 0,
+      rsGroup2.getTables().size());
+  }
+
+  @Test
+  public void testTableShouldNotAddedIntoRSGroup_WhenTableCreationFails() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+
+    // Create TableDescriptor without a family so creation fails
+    TableDescriptor desc =
+      TableDescriptorBuilder.newBuilder(tableName).setRegionServerGroup(rsGroup1.getName())
+        .build();
+    try {
+      admin.createTable(desc, getSpitKeys(5));
+      fail("Should have thrown DoNotRetryIOException but no exception thrown.");
+    } catch (DoNotRetryIOException e) {
+      assertTrue(
+        e.getCause().getMessage().contains("Table should have at least one column family"));
+    }
+    rsGroup1 = rsGroupAdmin.getRSGroupInfo(rsGroup1.getName());
+    assertEquals("Table must not have moved to RSGroup as table create operation failed", 0,
+      rsGroup1.getTables().size());
+  }
+
+  @Test
+  public void testSystemTablesCanBeMovedToNewRSGroupByModifyingTable() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+    final TableName tableName = TableName.valueOf("hbase:meta");
+    final TableDescriptor descriptor = admin.getConnection().getTable(tableName).getDescriptor();
+    final TableDescriptor newTableDescriptor =
+      TableDescriptorBuilder.newBuilder(descriptor).setRegionServerGroup(rsGroup1.getName())
+        .build();
+    admin.modifyTable(newTableDescriptor);
+    final RSGroupInfo rsGroupInfoOfTable = rsGroupAdmin.getRSGroupInfoOfTable(tableName);
+    assertEquals(rsGroup1.getName(), rsGroupInfoOfTable.getName());
+  }
+
+  @Test
+  public void testUpdateTableDescriptorOnlyIfRSGroupInfoWasStoredInTableDescriptor()
+    throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+    // create table with rs group info stored in table descriptor
+    createTable(tableName, rsGroup1.getName());
+    final TableName table2 = TableName.valueOf(tableName.getNameAsString() + "_2");
+    // create table with no rs group info
+    createTable(table2, null);
+    rsGroupAdmin.moveTables(Sets.newHashSet(tableName), rsGroup1.getName());
+    assertTrue("RSGroup info is not updated into TableDescriptor when table created",
+      admin.getConnection().getTable(tableName).getDescriptor().getRegionServerGroup()
+        .isPresent());
+    assertFalse("Table descriptor should not have been updated "
+        + "as rs group info was not stored in table descriptor.",
+      admin.getConnection().getTable(table2).getDescriptor().getRegionServerGroup().isPresent());
+
+    final String rsGroup2 = "rsGroup2";
+    rsGroupAdmin.renameRSGroup(rsGroup1.getName(), rsGroup2);
+    assertEquals(rsGroup2,
+      admin.getConnection().getTable(tableName).getDescriptor().getRegionServerGroup().get());
+    assertFalse("Table descriptor should not have been updated "
+        + "as rs group info was not stored in table descriptor.",
+      admin.getConnection().getTable(table2).getDescriptor().getRegionServerGroup().isPresent());
+  }
+
+  @Test
+  public void testModifyAndMoveTableScenario() throws Exception {
+    RSGroupInfo rsGroup1 = addGroup("rsGroup1", 1);
+    assertEquals(0, rsGroup1.getTables().size());
+    // create table with rs group info stored in table descriptor
+    createTable(tableName, rsGroup1.getName());
+    final TableName table2 = TableName.valueOf(tableName.getNameAsString() + "_2");
+    // create table with no rs group info
+    createTable(table2, null);
+    rsGroupAdmin.moveTables(Sets.newHashSet(table2), rsGroup1.getName());
+
+    RSGroupInfo rsGroup2 = addGroup("rsGroup2", 1);
+    rsGroupAdmin.moveTables(Sets.newHashSet(tableName, table2), rsGroup2.getName());
+    rsGroup2 = rsGroupAdmin.getRSGroupInfo(rsGroup2.getName());
+    assertEquals("Table movement failed.", 2, rsGroup2.getTables().size());
+  }
+
+  private void createTable(TableName tName, String rsGroupName) throws Exception {
+    ColumnFamilyDescriptor f1 = ColumnFamilyDescriptorBuilder.newBuilder(familyNameBytes).build();
+    final TableDescriptorBuilder builder =
+      TableDescriptorBuilder.newBuilder(tName).setColumnFamily(f1);
+    if (rsGroupName != null) {
+      builder.setRegionServerGroup(rsGroupName);
+    }
+    TableDescriptor desc = builder.build();
+    admin.createTable(desc, getSpitKeys(10));
+    TEST_UTIL.waitFor(WAIT_TIMEOUT, (Waiter.Predicate<Exception>) () -> {
+      List<String> regions = getTableRegionMap().get(tName);
+      if (regions == null) {
+        return false;
+      }
+
+      return getTableRegionMap().get(tName).size() >= 5;
+    });
+  }
+
+  private byte[][] getSpitKeys(int numRegions) throws IOException {
+    if (numRegions < 3) {
+      throw new IOException("Must create at least 3 regions");
+    }
+    byte[] startKey = Bytes.toBytes("aaaaa");
+    byte[] endKey = Bytes.toBytes("zzzzz");
+    return Bytes.split(startKey, endKey, numRegions - 3);
+  }
+}