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

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


The following commit(s) were added to refs/heads/branch-2.6 by this push:
     new 2762b45bd61 HBASE-30323 [RSGroup] Forward-port HBASE-22658 to branch-2 
(#8547)
2762b45bd61 is described below

commit 2762b45bd61531945aed11e4acc337f1a9f2e0cb
Author: Umesh <[email protected]>
AuthorDate: Thu Aug 20 10:03:26 2026 +0530

    HBASE-30323 [RSGroup] Forward-port HBASE-22658 to branch-2 (#8547)
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
    
    Signed-off-by: Viraj Jasani <[email protected]>
---
 .../rsgroup/TestRegionMoverWithRSGroupEnable.java  | 277 +++++++++++++++++++++
 .../org/apache/hadoop/hbase/util/RegionMover.java  |  47 +++-
 .../util/TestRegionMoverFilterRSGroupServers.java  | 158 ++++++++++++
 3 files changed, 481 insertions(+), 1 deletion(-)

diff --git 
a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRegionMoverWithRSGroupEnable.java
 
b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRegionMoverWithRSGroupEnable.java
new file mode 100644
index 00000000000..86b57e6e920
--- /dev/null
+++ 
b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRegionMoverWithRSGroupEnable.java
@@ -0,0 +1,277 @@
+/*
+ * 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.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.hbase.HBaseTestingUtility;
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
+import org.apache.hadoop.hbase.master.ServerManager;
+import org.apache.hadoop.hbase.net.Address;
+import org.apache.hadoop.hbase.regionserver.HRegion;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.util.JVMClusterUtil;
+import org.apache.hadoop.hbase.util.RegionMover;
+import org.apache.hadoop.hbase.util.RegionMover.RegionMoverBuilder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Tests that RegionMover.unloadRegions() respects RSGroup membership in 
branch-2: regions
+ * decommissioned from a server in a non-default RSGroup must land only on 
other servers in the same
+ * group, not on servers in unrelated groups.
+ */
+@Tag(MiscTests.TAG)
+@Tag(MediumTests.TAG)
+public class TestRegionMoverWithRSGroupEnable {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(TestRegionMoverWithRSGroupEnable.class);
+
+  private static final HBaseTestingUtility TEST_UTIL = new 
HBaseTestingUtility();
+  private static final String TEST_RSGROUP = "test";
+  private static final TableName TABLE_NAME = 
TableName.valueOf("testRegionMoverWithRSGroupEnable");
+
+  @BeforeAll
+  public static void setUpBeforeClass() throws Exception {
+    
TEST_UTIL.getConfiguration().set(HConstants.HBASE_MASTER_LOADBALANCER_CLASS,
+      RSGroupBasedLoadBalancer.class.getName());
+    
TEST_UTIL.getConfiguration().set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
+      RSGroupAdminEndpoint.class.getName());
+    
TEST_UTIL.getConfiguration().setInt(ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART,
 5);
+    TEST_UTIL.startMiniCluster(5);
+  }
+
+  @AfterAll
+  public static void tearDownAfterClass() throws Exception {
+    TEST_UTIL.shutdownMiniCluster();
+  }
+
+  // Addresses of the two servers placed in TEST_RSGROUP each test.
+  private final List<Address> rsservers = new ArrayList<>(2);
+  // Addresses of the servers that remain in the default group (excludes meta 
RS).
+  private final List<ServerName> defaultGroupServers = new ArrayList<>();
+  private RSGroupAdminClient rsGroupAdmin;
+  private ServerName rsContainMeta;
+
+  @BeforeEach
+  public void setUp() throws Exception {
+    rsGroupAdmin = new RSGroupAdminClient(TEST_UTIL.getConnection());
+    if (rsGroupAdmin.getRSGroupInfo(TEST_RSGROUP) == null) {
+      rsGroupAdmin.addRSGroup(TEST_RSGROUP);
+    }
+    Collection<ServerName> allServers = 
TEST_UTIL.getAdmin().getRegionServers();
+
+    // Exclude the RS that hosts hbase:meta to keep the test stable.
+    rsContainMeta = 
TEST_UTIL.getMiniHBaseCluster().getRegionServerThreads().stream()
+      .map(JVMClusterUtil.RegionServerThread::getRegionServer)
+      .filter(rs -> 
!rs.getRegions(TableName.META_TABLE_NAME).isEmpty()).findFirst().get()
+      .getServerName();
+    LOG.info("{} contains hbase:meta, keeping in default group", 
rsContainMeta);
+
+    // Move any leftover servers back to default before setting up fresh 
assignments.
+    RSGroupInfo existingGroup = rsGroupAdmin.getRSGroupInfo(TEST_RSGROUP);
+    if (existingGroup != null && !existingGroup.getServers().isEmpty()) {
+      rsGroupAdmin.moveServers(new HashSet<>(existingGroup.getServers()),
+        RSGroupInfo.DEFAULT_GROUP);
+    }
+
+    List<ServerName> modifiable = new ArrayList<>(allServers);
+    modifiable.remove(rsContainMeta);
+    int i = 0;
+    for (ServerName server : modifiable) {
+      if (i == 2) {
+        break;
+      }
+      rsservers.add(Address.fromParts(server.getHostname(), server.getPort()));
+      i++;
+    }
+    rsGroupAdmin.moveServers(new HashSet<>(rsservers), TEST_RSGROUP);
+    LOG.info("Servers moved to {} group: {}", TEST_RSGROUP, rsservers);
+
+    assertEquals(3, 
rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size());
+    assertEquals(2, 
rsGroupAdmin.getRSGroupInfo(TEST_RSGROUP).getServers().size());
+
+    // Record the three default-group servers (used for isolation assertions).
+    for (ServerName sn : allServers) {
+      Address addr = sn.getAddress();
+      if (!rsservers.contains(addr)) {
+        defaultGroupServers.add(sn);
+      }
+    }
+
+    if (TEST_UTIL.getAdmin().tableExists(TABLE_NAME)) {
+      TEST_UTIL.deleteTable(TABLE_NAME);
+    }
+    TableDescriptor tableDesc = TableDescriptorBuilder.newBuilder(TABLE_NAME)
+      .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f")).build();
+    TEST_UTIL.getAdmin().createTable(tableDesc, Bytes.toBytes("a"), 
Bytes.toBytes("z"), 9);
+    rsGroupAdmin.moveTables(new HashSet<>(Arrays.asList(TABLE_NAME)), 
TEST_RSGROUP);
+    TEST_UTIL.waitTableAvailable(TABLE_NAME);
+  }
+
+  @AfterEach
+  public void tearDown() throws Exception {
+    if (TEST_UTIL.getAdmin().tableExists(TABLE_NAME)) {
+      TEST_UTIL.deleteTable(TABLE_NAME);
+    }
+    if (!rsservers.isEmpty()) {
+      rsGroupAdmin.moveServers(new HashSet<>(rsservers), 
RSGroupInfo.DEFAULT_GROUP);
+    }
+    if (rsGroupAdmin.getRSGroupInfo(TEST_RSGROUP) != null) {
+      rsGroupAdmin.removeRSGroup(TEST_RSGROUP);
+    }
+    rsservers.clear();
+    defaultGroupServers.clear();
+    rsContainMeta = null;
+  }
+
+  /**
+   * Unloading a server in a non-default RSGroup must move all regions to the 
remaining server in
+   * that group — and must not move any region to a server in the default 
group.
+   */
+  @Test
+  public void testUnloadRegionsRespectsRSGroup() throws Exception {
+    // Regions are placed via randomAssignment across the test group's 
servers, so pick the
+    // decommission target as whichever rsservers member actually hosts a 
TABLE_NAME region —
+    // otherwise the test could pass without ever exercising the move/filter 
path.
+    HRegionServer hostingRS = 
TEST_UTIL.getMiniHBaseCluster().getRegionServerThreads().stream()
+      .map(JVMClusterUtil.RegionServerThread::getRegionServer)
+      .filter(rs -> rsservers.contains(rs.getServerName().getAddress()))
+      .filter(rs -> !rs.getRegions(TABLE_NAME).isEmpty()).findFirst().get();
+    Address decommission = hostingRS.getServerName().getAddress();
+    Address online =
+      rsservers.stream().filter(addr -> 
!addr.equals(decommission)).findFirst().get();
+    String filename = new Path(TEST_UTIL.getDataTestDir(), 
"testRSGroupUnload").toString();
+
+    RegionMoverBuilder builder =
+      new RegionMoverBuilder(decommission.toString(), 
TEST_UTIL.getConfiguration());
+    try (RegionMover rm = builder.filename(filename).ack(true).build()) {
+      LOG.info("Unloading {}", decommission.getHostname());
+      rm.unload();
+    }
+
+    HRegionServer onlineRS = 
TEST_UTIL.getMiniHBaseCluster().getRegionServerThreads().stream()
+      .map(JVMClusterUtil.RegionServerThread::getRegionServer)
+      .filter(rs -> 
rs.getServerName().getAddress().equals(online)).findFirst().get();
+
+    // Positive assertion: all 9 regions landed on the one remaining 
test-group server.
+    assertEquals(9, onlineRS.getNumberOfOnlineRegions(),
+      "All 9 regions must be on the single remaining server in the test 
RSGroup");
+
+    // Isolation assertion: no default-group server received any of the 
table's regions.
+    for (ServerName defaultSN : defaultGroupServers) {
+      HRegionServer defaultRS = 
TEST_UTIL.getMiniHBaseCluster().getRegionServerThreads().stream()
+        .map(JVMClusterUtil.RegionServerThread::getRegionServer)
+        .filter(rs -> 
rs.getServerName().equals(defaultSN)).findFirst().orElse(null);
+      if (defaultRS == null) {
+        continue;
+      }
+      List<HRegion> tableRegions = defaultRS.getRegions(TABLE_NAME);
+      assertTrue(tableRegions.isEmpty(), "Default-group server " + defaultSN
+        + " must not hold any regions of " + TABLE_NAME + " but had: " + 
tableRegions);
+    }
+  }
+
+  /**
+   * Unloading a server that is in the default RSGroup must still succeed 
end-to-end when RSGroups
+   * are enabled. Destinations must be filtered to the default group: regions 
may spread across the
+   * other default-group servers, but must not land on any test-group server.
+   */
+  @Test
+  public void testUnloadDefaultGroupServerWithRSGroupEnabled() throws 
Exception {
+    Admin admin = TEST_UTIL.getAdmin();
+    // Avoid unloading the meta-carrying server here too, for the same 
stability reason setUp()
+    // avoids it when picking rsservers.
+    ServerName defaultSN =
+      defaultGroupServers.stream().filter(sn -> 
!sn.equals(rsContainMeta)).findFirst().get();
+    Address decommission = defaultSN.getAddress();
+    String filename = new Path(TEST_UTIL.getDataTestDir(), 
"testDefaultGroupUnload").toString();
+
+    // Create a table in the default group; the balancer will distribute its 
regions naturally
+    // across the default-group servers, so defaultSN will hold at least some.
+    TableName defaultTable = TableName.valueOf("testDefaultGroupTable");
+    if (admin.tableExists(defaultTable)) {
+      TEST_UTIL.deleteTable(defaultTable);
+    }
+    try {
+      TableDescriptor td = TableDescriptorBuilder.newBuilder(defaultTable)
+        .setColumnFamily(ColumnFamilyDescriptorBuilder.of("f")).build();
+      admin.createTable(td, Bytes.toBytes("a"), Bytes.toBytes("z"), 6);
+      TEST_UTIL.waitTableAvailable(defaultTable);
+
+      HRegionServer decommRS = 
TEST_UTIL.getMiniHBaseCluster().getRegionServerThreads().stream()
+        .map(JVMClusterUtil.RegionServerThread::getRegionServer)
+        .filter(rs -> rs.getServerName().equals(defaultSN)).findFirst().get();
+      assertFalse(decommRS.getRegions(defaultTable).isEmpty(),
+        "Precondition: decommissioned server must actually host some regions 
of the default "
+          + "table, otherwise the post-unload check below is vacuous");
+
+      RegionMoverBuilder builder =
+        new RegionMoverBuilder(decommission.toString(), 
TEST_UTIL.getConfiguration());
+      try (RegionMover rm = builder.filename(filename).ack(true).build()) {
+        LOG.info("Unloading default-group server {}", 
decommission.getHostname());
+        rm.unload();
+      }
+
+      // After unload, the decommissioned server must hold no regions of the 
default table.
+      assertEquals(0, decommRS.getRegions(defaultTable).size(),
+        "Decommissioned default-group server must hold no regions after 
unload");
+
+      // Isolation assertion: no test-group server must hold any region of the 
default table.
+      // RegionMover must have restricted move targets to the default group 
only.
+      for (JVMClusterUtil.RegionServerThread rst : 
TEST_UTIL.getMiniHBaseCluster()
+        .getRegionServerThreads()) {
+        HRegionServer rs = rst.getRegionServer();
+        Address addr = rs.getServerName().getAddress();
+        if (rsservers.contains(addr)) {
+          List<HRegion> found = rs.getRegions(defaultTable);
+          assertTrue(found.isEmpty(), "Test-group server " + addr + " must not 
hold any region of "
+            + defaultTable + " but had: " + found);
+        }
+      }
+    } finally {
+      if (admin.tableExists(defaultTable)) {
+        TEST_UTIL.deleteTable(defaultTable);
+      }
+    }
+  }
+}
diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/RegionMover.java 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/RegionMover.java
index 6483cc78f4d..ef33ea9ce59 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/util/RegionMover.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/util/RegionMover.java
@@ -31,6 +31,7 @@ import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashSet;
@@ -55,6 +56,7 @@ import org.apache.hadoop.hbase.HBaseConfiguration;
 import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.HRegionLocation;
 import org.apache.hadoop.hbase.MetaTableAccessor;
+import org.apache.hadoop.hbase.RSGroupTableAccessor;
 import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.UnknownRegionException;
 import org.apache.hadoop.hbase.client.Admin;
@@ -67,6 +69,8 @@ import org.apache.hadoop.hbase.client.Result;
 import org.apache.hadoop.hbase.master.RackManager;
 import org.apache.hadoop.hbase.master.RegionState;
 import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
+import org.apache.hadoop.hbase.net.Address;
+import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
 import org.apache.hadoop.hbase.zookeeper.MetaTableLocator;
 import org.apache.hadoop.hbase.zookeeper.ZKWatcher;
 import org.apache.hadoop.hbase.zookeeper.ZNodePaths;
@@ -457,7 +461,13 @@ public class RegionMover extends AbstractHBaseTool 
implements Closeable {
       try {
         // Get Online RegionServers
         List<ServerName> regionServers = new ArrayList<>();
-        regionServers.addAll(admin.getRegionServers());
+        RSGroupInfo rsgroup = getRSGroupInfo(hostname, port);
+        if (rsgroup != null) {
+          LOG.info("{} belongs to RSGroup {}", hostname, rsgroup.getName());
+          regionServers.addAll(filterRSGroupServers(rsgroup, 
admin.getRegionServers()));
+        } else {
+          regionServers.addAll(admin.getRegionServers());
+        }
         // Remove the host Region server from target Region Servers list
         ServerName server = stripServer(regionServers, hostname, port);
         if (server == null) {
@@ -501,6 +511,8 @@ public class RegionMover extends AbstractHBaseTool 
implements Closeable {
         if (regionServers.isEmpty()) {
           LOG.warn("No Regions were moved - no servers available");
           return false;
+        } else {
+          LOG.info("Available servers {}", regionServers);
         }
         unloadRegions(server, regionServers, movedRegions, 
isolateRegionIdArray);
       } catch (Exception e) {
@@ -838,6 +850,39 @@ public class RegionMover extends AbstractHBaseTool 
implements Closeable {
     return servers;
   }
 
+  /**
+   * Returns the {@link RSGroupInfo} for the given host:port, or {@code null} 
if RSGroups are not in
+   * use on this cluster. If RSGroups are enabled but the server cannot be 
matched to any group,
+   * throws {@link IOException}.
+   */
+  private RSGroupInfo getRSGroupInfo(String host, int port) throws IOException 
{
+    if (!RSGroupTableAccessor.isRSGroupsEnabled(conn)) {
+      return null;
+    }
+    Address address = Address.fromParts(host, port);
+    for (RSGroupInfo rsGroupInfo : 
RSGroupTableAccessor.getAllRSGroupInfo(conn)) {
+      if (rsGroupInfo.containsServer(address)) {
+        return rsGroupInfo;
+      }
+    }
+    throw new IOException("RSGroups are enabled but no RSGroup contains " + 
host + ":" + port
+      + " — the server may not be registered, or its address form (hostname vs 
IP) may not "
+      + "match what the RS registered with");
+  }
+
+  @InterfaceAudience.Private
+  Collection<ServerName> filterRSGroupServers(RSGroupInfo rsgroup,
+    Collection<ServerName> onlineServers) {
+    List<ServerName> result = new ArrayList<>(rsgroup.getServers().size());
+    for (ServerName server : onlineServers) {
+      Address address = Address.fromParts(server.getHostname(), 
server.getPort());
+      if (rsgroup.containsServer(address)) {
+        result.add(server);
+      }
+    }
+    return result;
+  }
+
   /**
    * Designates or excludes the servername whose hostname and port portion 
matches the list given in
    * the file. Example:<br>
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestRegionMoverFilterRSGroupServers.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestRegionMoverFilterRSGroupServers.java
new file mode 100644
index 00000000000..a4d06350247
--- /dev/null
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestRegionMoverFilterRSGroupServers.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hadoop.hbase.HBaseTestingUtility;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.net.Address;
+import org.apache.hadoop.hbase.rsgroup.RSGroupInfo;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.util.RegionMover.RegionMoverBuilder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Unit tests for {@link RegionMover#filterRSGroupServers}. Spins up a 2-node 
mini cluster so we can
+ * build a real RegionMover instance; the method under test is pure in-memory 
logic. Branch-2's
+ * hbase-server module cannot reach {@code Admin.getRSGroup}/{@code 
moveServersToRSGroup} (those
+ * live in hbase-rsgroup, which depends on hbase-server, not the reverse), so 
{@link RSGroupInfo}
+ * instances here are constructed directly rather than read back from the 
master as in the
+ * master-branch equivalent of this test (see HBASE-30331 / #8552) -- the 
scenarios and intent
+ * match.
+ */
+@Tag(MiscTests.TAG)
+@Tag(MediumTests.TAG)
+public class TestRegionMoverFilterRSGroupServers {
+
+  private static final Logger LOG =
+    LoggerFactory.getLogger(TestRegionMoverFilterRSGroupServers.class);
+
+  private static final HBaseTestingUtility TEST_UTIL = new 
HBaseTestingUtility();
+
+  @BeforeAll
+  public static void setUpBeforeClass() throws Exception {
+    TEST_UTIL.startMiniCluster(2);
+  }
+
+  @AfterAll
+  public static void tearDownAfterClass() throws Exception {
+    TEST_UTIL.shutdownMiniCluster();
+  }
+
+  private RegionMover buildMover() throws Exception {
+    ServerName any = TEST_UTIL.getAdmin().getRegionServers().iterator().next();
+    return new RegionMoverBuilder(any.getHostname() + ":" + any.getPort(),
+      TEST_UTIL.getConfiguration()).build();
+  }
+
+  private static Address addressOf(ServerName sn) {
+    return Address.fromParts(sn.getHostname(), sn.getPort());
+  }
+
+  /**
+   * Reproduces HBASE-30331/HBASE-22740: a group named "default" whose real 
membership does not
+   * include every online server (e.g. because a server was moved out of it 
into a custom group)
+   * must still be filtered down to its actual members. filterRSGroupServers 
must not short-circuit
+   * on the group's name and return every online server regardless of 
membership.
+   */
+  @Test
+  public void testDefaultGroupFiltersToActualMembers() throws Exception {
+    try (RegionMover rm = buildMover()) {
+      List<ServerName> allServers = new 
ArrayList<>(TEST_UTIL.getAdmin().getRegionServers());
+      assertEquals(2, allServers.size(), "Mini cluster should have started 
with 2 region servers");
+
+      ServerName movedOut = allServers.get(0);
+      ServerName inDefault = allServers.get(1);
+
+      // Simulates the master-computed "default" group after movedOut was 
moved to another
+      // group: only inDefault remains, even though the group is still named 
"default".
+      RSGroupInfo defaultGroup = new RSGroupInfo(RSGroupInfo.DEFAULT_GROUP);
+      defaultGroup.addServer(addressOf(inDefault));
+
+      Collection<ServerName> result = rm.filterRSGroupServers(defaultGroup, 
allServers);
+
+      assertEquals(1, result.size(),
+        "filterRSGroupServers should return only the default group's actual 
members");
+      assertTrue(result.contains(inDefault),
+        "Server that is an actual member of the default group must be returned 
as a destination");
+      assertFalse(result.contains(movedOut),
+        "Server not in the default group's real membership must not be 
returned as a "
+          + "destination just because the group being filtered is named 
'default'");
+    }
+  }
+
+  /** A non-default group with one member must return only that member. */
+  @Test
+  public void testNonDefaultGroupFiltersToMembers() throws Exception {
+    try (RegionMover rm = buildMover()) {
+      List<ServerName> allServers = new 
ArrayList<>(TEST_UTIL.getAdmin().getRegionServers());
+      assertEquals(2, allServers.size(), "Mini cluster should have started 
with 2 region servers");
+
+      ServerName member = allServers.get(0);
+      ServerName other = allServers.get(1);
+      RSGroupInfo group = new RSGroupInfo("testgroup");
+      group.addServer(addressOf(member));
+
+      Collection<ServerName> result = rm.filterRSGroupServers(group, 
allServers);
+      assertEquals(1, result.size(),
+        "filterRSGroupServers should return only the non-default group's 
actual members");
+      assertTrue(result.contains(member),
+        "Server that is an actual member of the group must be returned as a 
destination");
+      assertFalse(result.contains(other),
+        "Server that is not a member of the group must not be returned as a 
destination");
+    }
+  }
+
+  /**
+   * A group's real member must not be returned as a destination when it is 
absent from the
+   * {@code onlineServers} snapshot handed to the filter (e.g. the server is 
currently offline or
+   * was already excluded upstream).
+   */
+  @Test
+  public void testGroupMemberAbsentFromOnlineServersReturnsEmpty() throws 
Exception {
+    try (RegionMover rm = buildMover()) {
+      List<ServerName> allServers = new 
ArrayList<>(TEST_UTIL.getAdmin().getRegionServers());
+      assertEquals(2, allServers.size(), "Mini cluster should have started 
with 2 region servers");
+
+      ServerName member = allServers.get(0);
+      ServerName other = allServers.get(1);
+      RSGroupInfo group = new RSGroupInfo("testgroup");
+      group.addServer(addressOf(member));
+
+      // The group's only member is not part of the online-servers snapshot 
passed in.
+      Collection<ServerName> result =
+        rm.filterRSGroupServers(group, Collections.singletonList(other));
+      assertTrue(result.isEmpty(),
+        "Group member absent from the online-servers snapshot must not be 
returned as a "
+          + "destination");
+    }
+  }
+}

Reply via email to